Compare commits

..

117 Commits

Author SHA1 Message Date
RiotRobot cf06547063 v34.12.0 2024-11-19 14:17:48 +00:00
RiotRobot 620dc2f6e2 v34.12.0-rc.0 2024-11-12 13:59:57 +00:00
Michael Telatynski 00d78077b0 Fix tests after security patches (#4513)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 11:16:03 +00:00
RiotRobot 23d2d03912 Merge branch 'master' into develop 2024-11-12 09:30:35 +00:00
RiotRobot b4c4355d1a v34.11.1 2024-11-12 09:30:01 +00:00
Michael Telatynski 142c0a65e6 Merge remote-tracking branch 'origin/develop' into develop 2024-11-12 09:27:54 +00:00
Michael Telatynski b9aacea1cb Fix release scripts
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/4496

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 09:27:44 +00:00
RiotRobot 76e653b7ee Merge branch 'master' into develop 2024-11-12 09:13:57 +00:00
RiotRobot 4d4ff4c3f2 v34.11.0 2024-11-12 09:13:26 +00:00
Michael Telatynski 00aba742e4 Merge commit from fork
to avoid path traversal attacks
and remove the legacy allowance for fragments in MXCs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 09:08:00 +00:00
Andrew Ferrazzutti 35d862ebd3 Handle M_MAX_DELAY_EXCEEDED errors (#4511)
* Handle M_MAX_DELAY_EXCEEDED errors

Use a lower delay time if the server rejects a delay as too long.

* Add test

* Lint test

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Robin <robin@robin.town>

* Test computed expiry timeout value

---------

Co-authored-by: Robin <robin@robin.town>
2024-11-11 20:48:53 +00:00
Hugh Nimmo-Smith 581b3209ab Allow configuration of MatrixRTC timers when calling joinRoomSession() (#4510) 2024-11-11 15:35:05 +00:00
Andrew Ferrazzutti 6855ace642 When state says you've left ongoing call, rejoin (#4342)
* When state says you've left ongoing call, rejoin

When receiving a state change that says you are no longer a member of a
RTC session that you are actually still participating in, send another
state event to put yourself back in the session membership.

This can happen when an administrator overwrites your call membership
event (which is allowed even with MSC3757's restrictions on state), or
if your delayed disconnection event (via MSC4140) timed out before your
client could send a heartbeat to delay it further.

* Don't emit state changed on join recovery
2024-11-11 15:07:33 +00:00
Florian Duros 5033d48013 Fix tsdoc error in rust-crypto folder (#4504) 2024-11-11 13:15:46 +00:00
Andrew Ferrazzutti 4c53836a13 Remove redundant type arguments in function call (#4507)
as the types can be deduced by the function arguments.
2024-11-11 11:19:48 +00:00
Andrew Ferrazzutti 10a4fd8328 MatrixRTCSession: handle rate limit errors (#4494)
* MatrixRTCSession: handle rate limit errors

* Lint

* Handle ratelimiting for non-legacy state setting

Each request must be retried, as the non-legacy flow involves a sequence
of requests that must resolve in order.

* Fix broken test

* Check for MSC3757 instead of the unmerged MSC3779

* Move helper out of beforeEach

* Test ratelimit errors
2024-11-11 02:55:42 +00:00
Andrew Ferrazzutti 98f7637683 Send/receive error details with widgets (#4492)
* Send/receive error details with widgets

* Fix embedded client tests

* Use all properties of error responses

* Lint

* Rewrite ternary expression as if statement

* Put typehints on overridden functions

* Lint

* Update matrix-widget-api

* Don't @link across packages

as gendoc fails when doing so.

* Add a missing docstring

* Set widget response error string to correct value

* Test conversion to/from widget error payloads

* Test processing errors thrown by widget transport

* Lint

* Test processing errors from transport.sendComplete
2024-11-09 07:29:04 +00:00
Florian Duros 0df8e81da4 Deprecate MatrixClient.getKeyBackupVersion (#4505) 2024-11-08 09:06:09 +00:00
Florian Duros 30b1894f37 Deprecate unused callback in CryptoCallbacks (#4501) 2024-11-06 15:38:15 +00:00
Richard van der Hoff fbbdb6e766 Remove dead release scripts (#4496)
* Remove redundant `pre-release.sh` script

This is now a no-op (there are no `matrix_lib` fields in package.json), so we
may as well remove it.

* Remove redundant `post-merge-master` script

Just as pre-release is a no-op, so is this

* Remove redundant switch_package_to_release script

Once more: this script is a no-op.
2024-11-05 16:38:13 +00:00
RiotRobot 5a1488ebd5 Merge branch 'master' into develop 2024-11-05 13:49:42 +00:00
RiotRobot c4048d985d v34.10.0 2024-11-05 13:49:13 +00:00
David Baker 794f044dda Make doc clearer on getCrossSigningKeyId (#4477)
* Make doc clearer on getCrossSigningKeyId

I was trying to work out why this was being used in a check. It
turns out it only returns the key ID if the private part is stored
locally, which seems very much non-obvious.

* Better doc

* Formatting & clarity

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

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-04 10:53:30 +00:00
Michael Telatynski a197afe8aa Refactor MatrixClient::forget to not abuse membershipChange API (#4490)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 09:49:27 +00:00
Florian Duros 1061b93b29 Remove remaining legacy crypto imports in new crypto and tests (#4491)
* Use `CryptoCallbacks` from `CryptoApi` instead of legacy crypto.

* Use `KeyBackup` from rust crypto instead of `IKeyBackup` from legacy crypto
2024-11-04 08:55:32 +00:00
Hugh Nimmo-Smith 6528a59fc1 Reduce cognitive complexity of Room.addLiveEvents() (#4493) 2024-11-01 17:38:08 +00:00
Will Hunt f6a169b5a5 Replace usages of global with globalThis (#4489)
* Update src with globalThis

* Update spec with globalThis

* Replace in more spec/ places

* More changes to src/

* Add a linter rule for global

* Prettify

* lint
2024-11-01 09:15:21 +00:00
renovate[bot] d04135cc1c Update dependency uuid to v11 (#4482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 11:39:59 +00:00
Andrew Ferrazzutti 546047a050 Capture HTTP error response headers & handle Retry-After header (MSC4041) (#4471)
* Include HTTP response headers in MatrixError

* Lint

* Support MSC4041 / Retry-After header

* Fix tests

* Remove redundant MatrixError parameter properties

They are inherited from HTTPError, so there is no need to mark them as
parameter properties.

* Comment that retry_after_ms is deprecated

* Properly handle colons in XHR header values

Also remove the negation in the if-condition for better readability

* Improve Retry-After parsing and docstring

* Revert ternary operator to if statements

for readability

* Reuse resolved Headers for Content-Type parsing

* Treat empty Content-Type differently from null

* Add MatrixError#isRateLimitError

This is separate from MatrixError#getRetryAfterMs because it's possible
for a rate-limit error to have no Retry-After time, and having separate
methods to check each makes that more clear.

* Ignore HTTP status code when getting Retry-After

because status codes other than 429 may have Retry-After

* Catch Retry-After parsing errors

* Add test coverage for HTTP error headers

* Update license years

* Move safe Retry-After lookup to global function

so it can more conveniently check if an error is a MatrixError

* Lint

* Inline Retry-After header value parsing

as it is only used in one place and doesn't need to be exported

* Update docstrings

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

* Use bare catch

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

* Give HTTPError methods for rate-limit checks

and make MatrixError inherit them

* Cover undefined errcode in rate-limit check

* Update safeGetRetryAfterMs docstring

Be explicit that errors that don't look like rate-limiting errors will
not pull a retry delay value from the error.

* Use rate-limit helper functions in more places

* Group the header tests

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-10-30 15:52:34 +00:00
Florian Duros 16153e5d82 Replace legacy keyBackup types (#4486) 2024-10-30 13:12:27 +00:00
Hugh Nimmo-Smith fd73d5068c Add RoomWidgetClient.sendToDeviceViaWidgetApi() (#4475) 2024-10-30 09:36:44 +00:00
Richard van der Hoff e72859a44a Update codeowners for crypto-api (#4478)
Obviously, the crypto api is owned by the (web) crypto team!
2024-10-29 17:02:14 +00:00
RiotRobot a8f8a9c14d v34.10.0-rc.0 2024-10-29 12:55:48 +00:00
Florian Duros 0e2f73d7a7 Deprecate CreateSecretStorageOpts.keyBackupInfo used in CryptoApi.bootstrapSecretStorage. (#4474) 2024-10-28 12:00:39 +00:00
Hugh Nimmo-Smith 31aeb3044f Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() (#4380)
* Add CryptoApi. encryptToDeviceMessages

Deprecate Crypto. encryptAndSendToDevices and MatrixClient. encryptAndSendToDevices

* Overload MatrixClient. encryptAndSendToDevices instead of deprecating

* Revert "Overload MatrixClient. encryptAndSendToDevices instead of deprecating"

This reverts commit 6a0d8e26385c34d40e8c2ed1e34cb5119c12456c.

* Feedback from code review

* Use temporary pre-release build of @matrix-org/matrix-sdk-crypto-wasm

* Deduplicate user IDs

* Test for RustCrypto implementation

* Use ensureSessionsForUsers()

* Encrypt to-device messages in parallel

* Use release version of matrix-sdk-crypto-wasm

* Upgrade matrix-sdk-crypto-wasm to v8

* Sync with develop

* Add test for olmlib CryptoApi

* Fix link

* Feedback from review

* Move libolm implementation to better place in file

* FIx doc

* Integration test

* Make sure test device is known to client

* Feedback from review
2024-10-28 11:32:17 +00:00
Hugh Nimmo-Smith 0a29063bc9 Do not rotate MatrixRTC media encryption key when a new member joins a session (#4472)
* Do not rotate MatrixRTC media encryption key when a new member joins a call

This change reverts https://github.com/matrix-org/matrix-js-sdk/pull/4422.

Instead, the rotation when a new member joins will be reintroduced as part of supporting to-device based MatrixRTC encryption key distribution.

* Improve function name
2024-10-25 13:32:44 +00:00
Richard van der Hoff 3cc3bd0728 Avoid <sender>|<session> notation in log messages (#4473)
We used to use the notation `<sender key>|<megolm session id>` fairly widely in
log messages, but since the transition to rust crypto, it's unusual and now
somewhat confusing. Make the log messages more explicit.
2024-10-25 12:48:44 +00:00
Michael Telatynski f891fe4423 Fix gitflow workflow not handling edge case (#4463)
of not needing to reset package to `#develop`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-25 11:15:00 +00:00
Andrew Ferrazzutti b99ff83785 Refactor/simplify Promises in MatrixRTCSession (#4466)
* Refactor/simplify Promises in MatrixRTCSession

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

* Fix+document+test leaveRoomSession's return value

* Throw instead of using expect in teardown

because lint rules forbid using expect outside of test functions

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2024-10-25 09:24:52 +00:00
Michael Telatynski 23c4c9fd8a Remove abandoned MSC3886, MSC3903, MSC3906 implementations (#4469)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-24 13:19:29 +00:00
RiotRobot 8b8ee91210 Merge branch 'master' into develop 2024-10-22 11:58:31 +00:00
RiotRobot d8c431f23e v34.9.0 2024-10-22 11:58:01 +00:00
Hugh Nimmo-Smith a6fb7530cb Organise MatrixRTCSession tests (#4453)
None of the tests themselves should have changed
2024-10-21 16:24:02 +00:00
Florian Duros b5c3f15a67 Deprecate MatrixClient.getDehydratedDevice (#4467) 2024-10-21 13:21:05 +00:00
renovate[bot] 91f6f0f9c5 Update typescript-eslint monorepo to v8 (major) (#4388)
* Update typescript-eslint monorepo to v8

* Migrate to stylistic

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>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-21 13:17:20 +00:00
renovate[bot] 88cf5eb769 Update actions/checkout digest to eef6144 (#4464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 13:54:33 +00:00
Andrew Ferrazzutti 13a967ae8f Prepare delayed call leave events more reliably (#4447)
* Prepare delayed call leave events more reliably

- Try sending call join after preparing delayed leave
- On leave, send delayed leave instead of a new event

* Don't rely on errcodes for retry logic

because they are unavailable in widget mode

* Make arrow method readonly

SonarCloud rule typescript:S2933

* Test coverage for restarting delayed call leave

* Remove unneeded unstable_features mock

It's unneeded because all affected methods are mocked
2024-10-17 17:41:18 +00:00
Michael Telatynski 66c80949e8 Pin GHA runner versions (#4461)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-17 09:04:19 +00:00
renovate[bot] 3d51e31da8 Update babel monorepo (#4454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 14:27:13 +00:00
renovate[bot] 86c190fa0d Update dependency @types/node to v18.19.55 (#4455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 14:21:56 +00:00
renovate[bot] e9a9280e3c Update all non-major dependencies (#4458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 13:34:51 +00:00
renovate[bot] fabdd6da64 Update dependency eslint-plugin-unicorn to v56 (#4459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 13:30:52 +00:00
renovate[bot] ab5d95102c Update dependency typescript to v5.6.3 (#4456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 12:51:13 +00:00
renovate[bot] b4556d6552 Update typedoc (#4457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 12:50:55 +00:00
renovate[bot] 6c22da9a96 Update dependency eslint to v8.57.1 (#4437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-16 12:50:08 +00:00
renovate[bot] d29329bddb Replace dependency eslint-plugin-node with eslint-plugin-n ^14.0.0 (#4416)
* Replace dependency eslint-plugin-node with eslint-plugin-n ^14.0.0

* Update eslint config

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-10-16 12:49:22 +00:00
Michael Telatynski 68b06d7b60 Update workflows referencing matrix-react-sdk to use only element-web (#4451) 2024-10-16 13:48:21 +01:00
Andrew Ferrazzutti 5f599ee978 Fix DelayedEventInfo type (#4446)
* Fix DelayedEventInfo type

for MSC4140's GET /delayed_events

* Satisfy linter while avoiding unaligned indents

* Remove transaction_id from DelayedEventInfo

See matrix-org/matrix-spec-proposals@883e6b5d
2024-10-15 21:06:36 +00:00
Florian Duros aabd558bce Deprecate top level crypto event re-export (#4444) 2024-10-15 15:00:43 +00:00
Florian Duros 662b772c73 Add crypto events to crypto-api (#4443)
* Move used Crypto event into crypto api

* Use new crypto events in rust crypto

* Remove `WillUpdateDevices` event from CryptoApi

* Use new crypto events in old crypto events

* Compute type of CryptoEvent enum

* Rename CryptoEvent and CryptoEventHandlerMap as legacy

* - Rename `RustCryptoEvent` as `CryptoEvent`
- Declare `CryptoEventHandlerMap` into the crypto api

* Add `WillUpdateDevices` back to new crypto events to avoid circular imports between old crypto and the cryto api

* Extends old crypto handler map with the new crypto map

* Review fixes

* Add more explicit documentations
2024-10-15 14:38:33 +00:00
RiotRobot b240c44128 v34.9.0-rc.0 2024-10-15 14:31:37 +00:00
RiotRobot 5508993d79 Merge branch 'master' into develop 2024-10-15 10:53:49 +00:00
RiotRobot c9b43ab251 v34.8.0 2024-10-15 10:53:23 +00:00
David Baker 2fb1e659c8 Merge commit from fork
Remove insecure MatrixClient.sendSharedHistoryKeys method
2024-10-15 11:48:16 +01:00
RiotRobot b842192a34 Merge branch 'master' into develop 2024-10-08 12:23:01 +00:00
RiotRobot 868bbfcb31 v34.7.0 2024-10-08 12:22:28 +00:00
Saul 860161bdd2 Fix the rust crypto import in esm environments. (#4445)
* Configure babel to fix the rust import in esm environments.

* Add lockfile changes.

* Cleanup rust-crypto import and babel config.
2024-10-08 10:34:01 +00:00
Florian Duros 0c9d82e40a Deprecate the crypto events which are not used by the rust-crypto (#4442)
* Deprecate crypto event not used by the rust-crypto

* Deprecate `Crypto.WillUpdateDevices` for `Crypto.DevicesUpdated`
2024-10-07 12:26:56 +00:00
Richard van der Hoff 73ab7c342a Expose crypto-api as its own typedoc module (#4439)
Currently the crypto-api hierarchy is exposed only as a `Crypto` namespace
under the "matrix" entrypoint in the documentation.

This isn't really right: it's meant to be a separate entrypoint (in the same
way as `types`, `testing` and `utils` are). This PR fixes that problem.
2024-10-07 11:38:28 +00:00
Hugh Nimmo-Smith 3386c66b98 Fix MatrixRTC sender key wrapping (#4441) 2024-10-07 10:34:23 +00:00
Florian Duros da044820d7 Clean AES export and move back calculateKeyCheck to secret-storage.ts (#4440) 2024-10-03 13:20:56 +00:00
renovate[bot] 9f40f32b3e Update dependency @types/node to v18.19.54 (#4436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:37:25 +00:00
renovate[bot] f679942584 Update all non-major dependencies (#4438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:16:22 +00:00
RiotRobot c1b4f97372 v34.7.0-rc.0 2024-10-01 14:54:58 +00:00
Florian Duros 5f3b89990d Move out crypto/aes (#4431)
* Move `SecretEncryptedPayload` in `src/utils/@types`

* Move `encryptAES` to a dedicated file. Moved in a utils folder.

* Move `deriveKeys` to a dedicated file in order to share it

* Move `decryptAES` to a dedicated file. Moved in a utils folder.

* Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder.

* Remove AES functions in `aes.ts` and export new ones for backward compatibility

* Update import to use new functions

* Add `src/utils` entrypoint in `README.md`

* - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`.
- Move into `src/@types`

* Move `calculateKeyCheck` into `secret-storage.ts`.

* Move `deriveKeys` into `src/utils/internal` folder.

* - Rename `encryptAES` on `encryptAESSecretStorageItem`
- Change named export by default export

* - Rename `decryptAES` on `decryptAESSecretStorageItem`
- Change named export by default export

* Update documentation

* Update `decryptAESSecretStorageItem` doc

* Add lnk to spec for `calculateKeyCheck`

* Fix downstream tests
2024-10-01 13:52:59 +00:00
Richard van der Hoff 866fd6f4a3 Bump matrix-rust-sdk to 9.1.0 (#4435)
Mostly, to pick up https://github.com/matrix-org/matrix-rust-sdk/pull/3985
2024-09-30 16:46:33 +00:00
Valere 9ecb66e695 crypto: configure key sharing strategy based on DeviceIsolationMode (#4425)
* crypto: configure key sharing strategy based on deviceIsolationMode

fix eslint import error

cryptoMode was renamed to deviceIsolationMode

post rebase fix: Device Isolation mode name changes

* Fix outdated docs referring to old cryptomode

* code review: better comment for globalBlacklistUnverifiedDevices option

* RoomEncryptor: Use appropriate default for getBlacklistUnverifiedDevices

* do not provide a default value for DeviceIsolationMode for encryption

* Update src/rust-crypto/RoomEncryptor.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-30 14:26:34 +00:00
Hugh Nimmo-Smith baa6d13506 RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() (#4427)
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys()

* Clarify comment

* Feedback from code review

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

* Fix test

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-09-27 15:55:07 +00:00
Hugh Nimmo-Smith 2d6230f199 Rotate RTC key when a new member joins (#4422) 2024-09-27 15:54:48 +00:00
Tulir Asokan 825d85f18d Update media event content types to include captions (#4403)
Signed-off-by: Tulir Asokan <tulir@maunium.net>
2024-09-26 16:06:59 +00:00
Richard van der Hoff 1dcb7a6e75 Remove insecure MatrixClient.sendSharedHistoryKeys method
This method is impossible to use securely, and so is being removed. (It also
didn't work under Rust cryptography.)

In future, this functionality may be re-introduced in a safer way, but doing so
will probably require updates to the MSC.
2024-09-26 12:03:45 +01:00
Richard van der Hoff 823316b2ff Remove use of insecure sendSharedHistoryKeys in MSC3089 impl 2024-09-26 12:01:51 +01:00
renovate[bot] d56fa197d0 Update dependency typescript to v5.6.2 (#4420)
* Update dependency typescript to v5.6.2

* Fix TS errors

* Update minimal version of TS to `5.4.2` since the code is not compliant with an older version.

* Review fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2024-09-26 08:23:38 +00:00
Michael Telatynski f7229bfff0 Update OIDC registration types to match latest MSC2966 state (#4432)
* Update OIDC registration types to match latest MSC2966 state

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

* Add comment

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-09-25 16:30:13 +00:00
Valere 538717c23e crypto: Replace cryptoMode with DeviceIsolationMode concept (#4429)
* crypto: Replace cryptoMode with DeviceIsolationMode concept

* use enum instead of string for the IsolationMode kind

* Code review - Cleaning, renaming

* review: unneeded @see in doc

* review: Rename IsolationMode with better names

* review: quick cleaning and doc
2024-09-25 13:33:02 +00:00
Richard van der Hoff 1a8ea3d685 Add CryptoApi.pinCurrentUserIdentity and UserIdentity.needsUserApproval (#4415)
* Implement `UserVerificationStatus.needsUserApproval`

Expose the `identityNeedsUserApproval` flag from the rust crypto crate.

* Add CryptoApi.pinCurrentUserIdentity

Expose `pinCurrentMasterKey` from the rust crypto api.

* Test data: add second cross-signing key for Bob

* Add tests for verification status
2024-09-24 16:38:18 +00:00
Florian Duros d0890d9450 Update sonarcloud action to use v3.3 matrix-org/sonarcloud-workflow-action (#4430) 2024-09-24 15:35:40 +00:00
RiotRobot 42ec17b977 Merge branch 'master' into develop 2024-09-24 12:37:47 +00:00
RiotRobot 82e9eefce6 v34.6.0 2024-09-24 12:37:19 +00:00
renovate[bot] f8208b1891 Update typedoc (#4419)
* Update typedoc

* Don't link a private method in tsdoc of a public method

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
2024-09-23 13:47:49 +00:00
Richard van der Hoff 092a59af66 Throw clearer error if createSecretStorageKey misbehaves (#4412)
https://github.com/element-hq/element-web/issues/27888 shows a slightly
mysterious error message, but the problem is `createSecretStorageKey`
returning the wrong thing.
2024-09-23 10:25:42 +00:00
renovate[bot] dbd7d26968 Update dependency @types/node to v18.19.50 (#4418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 15:26:50 +00:00
renovate[bot] 4fda9e8419 Update all non-major dependencies (#4417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 13:38:54 +00:00
RiotRobot a13e0389db v34.6.0-rc.0 2024-09-18 13:31:13 +00:00
Hubert Chathi dbb4828eda Add crypto mode setting for invisible crypto, and apply it to decrypting events (#4407)
Adds a global "crypto mode" setting to the crypto API (only works with Rust crypto), and changes the decryption settings based on that.
2024-09-18 12:53:07 +01:00
Hugh Nimmo-Smith 414ac9d8cc Don't share full key history for RTC per-participant encryption (#4406)
* Don't share full key history for RTC per-participant encryption

Also record stats for how many keys have been sent/received and age of those received

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Robin <robin@robin.town>

* Add comment about why we track total age of events

---------

Co-authored-by: Robin <robin@robin.town>
2024-09-18 08:57:26 +00:00
Johannes Marbach 30058a4bdc Add ESLint rule to ensure .ts extensions on imports (#4409)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-18 07:40:01 +00:00
Florian Duros fab9cab3df Move crypto/key_passphrase.ts to crypto-api/key-passphrase.ts (#4401)
* Move `crypto/key_passphrase.ts` to `crypto-api/key-passphrase.ts`

* Re-export `crypto-api/key-passphrase` into `crypto/key_passphrase.ts`

* Add doc

* Deprecate `MatrixClient.keyBackupKeyFromPassword`

* Move `keyFromAuthData` to `common-crypto/key-passphrase.ts`

* Fix faulty import

* Keep `keyFromPassphrase` in old crypto

* - Rename `deriveKey` into `deriveRecoveryKeyFromPassphrase`
- Call `deriveRecoveryKeyFromPassphrase` into `RustCrypto.createRecoveryKeyFromPassphrase` instead of using `keyFromPassphrase`

* Remove alternative in `keyBackupKeyFromPassword` deprecation.

* Add tests for `keyFromAuthData`

* Deprecate `keyFromAuthData`

* Review changes
2024-09-17 13:05:47 +00:00
Johannes Marbach 53b599f8fe Export membership types (#4405)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-14 10:44:51 +00:00
Timo 17f6cc733e Fix redact sending over in embedded (widget) mode (#4398) 2024-09-13 16:07:44 +00:00
Johannes Marbach c8403f39aa Fix build failure in node.js example (#4394)
* Fix node.js example

Relates to: element-hq/element-web#26922
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Update examples/node/app.js

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

* Update examples/node/package.json

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

* Move imports to the top of the file

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-13 15:35:45 +00:00
Florian Duros 8cf5df73ee Move crypto/recoverykey.ts to crypto-api/recovery-key.ts (#4399)
* Move `crypto/recoverykey.ts` to `crypto-api/recovery-key.ts`

* Re-export `crypto-api/recovery-key` into `crypto/recoverykey`

* Add a bit of doc

* Deprecate `MatrixClient.isValidRecoveryKey` and `MatrixClient.keyBackupKeyFromRecoveryKey`

* Import `index.ts` directly

* Update `recovery-key.ts` doc

* Add tests for `decodeRecoveryKey`

* Move `recovery-key.spec.ts` file
2024-09-13 13:26:30 +00:00
Florian Duros febe27ddcc Replace matrix.org/matrix-react-sdk by element-hq/matrix-react-sdk (#4404) 2024-09-13 13:03:31 +00:00
Hubert Chathi 7987ce76ec Bump dependency on rust-sdk-crypto-wasm to v8.0.0 (#4396) 2024-09-13 10:22:33 +01:00
Richard van der Hoff 60cedf2fdb Improve documentation on {encrypt,decrypt}AES (#4397) 2024-09-11 14:13:12 +00:00
Robin ed44514974 Expose the event ID of a call membership (#4395)
This is in line with the other information we're already exposing, such as the event's sender and timestamp. We want this in order to play around with adding reactions to the membership event.
2024-09-10 20:15:07 +00:00
RiotRobot 9f8c1ee953 Merge branch 'master' into develop 2024-09-10 12:31:32 +00:00
Will Hunt e8128d34a1 MSC4133 - Extended profiles (#4391)
* Add MSC4133 functionality.

* Add MSC4133 capability.

* Tidy

* Add tests for extended profiles.

* improve docs

* undefined

* Add a prefix function to reduce reptitiveness

* Add a docstring
2024-09-09 12:06:38 +00:00
Richard van der Hoff ba7bd06295 Element-R: Mark unsupported MatrixClient methods as deprecated (#4389)
Slap a `@deprecated` tag on all the MatrixClient methods which no longer work
with rust crypto.
2024-09-05 13:37:08 +00:00
Richard van der Hoff e4db6008b8 Minor improvements to logging in device verification (#4390)
A grab-bag of small logging improvements in the Rust crypto wrapper.
2024-09-05 10:22:54 +00:00
Johannes Marbach 52f35409ec Stabilise MSC4156 (#4381)
* Stabilise MSC4156

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

* Add deprecation comments

* Add minimum Matrix version

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-04 12:01:33 +00:00
renovate[bot] f50aab37c3 Update babel monorepo (#4387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:29:21 +00:00
renovate[bot] df0f817f83 Update dependency typedoc-plugin-mdn-links to v3.2.10 (#4386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:20:01 +00:00
renovate[bot] 7efa6352f8 Update shogo82148/actions-upload-release-asset digest to aac270e (#4385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:50 +00:00
renovate[bot] f74614705e Update mheap/github-action-required-labels digest to d25134c (#4384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:29 +00:00
renovate[bot] 169e8f8613 Update dependency @types/node to v18.19.47 (#4383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 15:22:51 +00:00
renovate[bot] f2f77bd1f7 Update all non-major dependencies (#4382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 13:51:09 +00:00
183 changed files with 6600 additions and 5567 deletions
+15 -5
View File
@@ -1,5 +1,5 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc"],
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
@@ -63,6 +63,10 @@ module.exports = {
name: "setImmediate",
message: "Use setTimeout instead.",
},
{
name: "global",
message: "Use globalThis instead.",
},
],
"import/no-restricted-paths": [
@@ -108,10 +112,8 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
// We do this sometimes to brand interfaces
"@typescript-eslint/no-empty-object-type": "off",
"quotes": "off",
// We use a `logger` intermediary module
@@ -128,6 +130,14 @@ module.exports = {
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
// Ensure .ts extension on imports outside of tests
"n/file-extension-in-import": [
"error",
"always",
{
tryExtensions: [".ts"],
},
],
},
},
{
+1
View File
@@ -8,6 +8,7 @@
/spec/*/webrtc @matrix-org/element-call-reviewers
/spec/*/matrixrtc @matrix-org/element-call-reviewers
/src/crypto-api @matrix-org/element-crypto-web-reviewers
/src/crypto @matrix-org/element-crypto-web-reviewers
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # 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@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@v4
@@ -21,14 +21,13 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
pull-requests: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
# 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 -3
View File
@@ -14,10 +14,8 @@ jobs:
include:
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
+5 -5
View File
@@ -12,9 +12,9 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || gi
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -29,7 +29,7 @@ jobs:
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
pull-requests: read
steps:
@@ -42,7 +42,7 @@ jobs:
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
steps:
- name: Check membership
@@ -69,7 +69,7 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
@@ -10,7 +10,7 @@ on:
concurrency: release-drafter-action
jobs:
draft:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
+4 -8
View File
@@ -15,7 +15,7 @@ on:
concurrency: ${{ github.workflow }}
jobs:
merge:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -49,9 +49,6 @@ jobs:
git checkout develop
git merge -X ours master
- name: Run post-merge-master script to revert package.json fields
run: ./.action-repo/scripts/release/post-merge-master.sh
- name: Reset dependencies
if: inputs.dependencies
run: |
@@ -61,26 +58,25 @@ jobs:
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
echo "Current $PACKAGE version is $CURRENT_VERSION"
if [ "$CURRENT_VERSION" == "null" ]
if [[ "$CURRENT_VERSION" == "null" ]]
then
echo "Unable to find $PACKAGE in package.json"
exit 1
fi
if [ "$CURRENT_VERSION" == "develop" ]
if [[ "$CURRENT_VERSION" == *"#develop" ]]
then
echo "Not updating dependency $PACKAGE"
continue
fi
echo "Resetting $1 to develop branch..."
echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
env:
DEPENDENCIES: ${{ inputs.dependencies }}
FINAL: ${{ inputs.final }}
- name: Push changes
run: git push origin develop
+6 -9
View File
@@ -41,7 +41,7 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
environment: Release
steps:
- name: Load GPG key
@@ -136,7 +136,9 @@ jobs:
done
- name: Bump package.json version
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}"
git add package.json
- name: Add to CHANGELOG.md
if: inputs.final
@@ -158,11 +160,6 @@ jobs:
env:
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
- name: Run pre-release script to update package.json fields
run: |
./.action-repo/scripts/release/pre-release.sh
git add package.json
- name: Commit changes
run: git commit -m "$VERSION"
@@ -276,7 +273,7 @@ jobs:
post-release:
name: Post release steps
needs: release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- id: repository
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
@@ -304,7 +301,7 @@ jobs:
bump-downstreams:
name: Update npm dependency in downstream projects
needs: npm
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: inputs.downstreams
strategy:
matrix:
+5 -1
View File
@@ -11,7 +11,7 @@ on:
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
@@ -39,6 +39,10 @@ jobs:
tag: next
ignore-scripts: false
- name: Check npm package was published
if: steps.npm-publish.outputs.id == ''
run: exit 1
- name: 🎖️ Add `latest` dist-tag to final releases
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
downstreams: '["element-hq/element-web"]'
docs:
name: Publish Documentation
needs: release
if: inputs.docs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
@@ -60,7 +60,7 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: docs
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
+3 -3
View File
@@ -14,7 +14,7 @@ on:
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
jobs:
sonarqube:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'merge_group'
@@ -30,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -63,7 +63,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v3.2
uses: matrix-org/sonarcloud-workflow-action@v3.3
# 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:
+5 -15
View File
@@ -11,7 +11,7 @@ concurrency:
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -26,19 +26,9 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -55,7 +45,7 @@ jobs:
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -72,7 +62,7 @@ jobs:
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -97,7 +87,7 @@ jobs:
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
+9 -9
View File
@@ -13,7 +13,7 @@ env:
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
matrix:
@@ -68,15 +68,15 @@ jobs:
name: Jest tests
needs: jest
if: always()
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1
matrix-react-sdk:
name: Downstream test matrix-react-sdk
element-web:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
uses: element-hq/element-web/.github/workflows/tests.yml@develop
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
@@ -91,7 +91,7 @@ jobs:
# 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
runs-on: ubuntu-24.04
if: always()
needs:
- complement-crypto
@@ -103,10 +103,10 @@ jobs:
# and skip sonarcloud coverage within merge queues
downstream:
name: Downstream tests
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: always()
needs:
- matrix-react-sdk
- element-web
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
@@ -119,5 +119,5 @@ jobs:
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
run: exit 1
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
automate-project-columns-next:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@main
with:
+99
View File
@@ -1,3 +1,102 @@
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
====================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.getKeyBackupVersion` ([#4505](https://github.com/matrix-org/matrix-js-sdk/pull/4505)). Contributed by @florianduros.
* Deprecate unused callbacks in `CryptoCallbacks` ([#4501](https://github.com/matrix-org/matrix-js-sdk/pull/4501)). Contributed by @florianduros.
## ✨ Features
* Handle M\_MAX\_DELAY\_EXCEEDED errors ([#4511](https://github.com/matrix-org/matrix-js-sdk/pull/4511)). Contributed by @AndrewFerr.
* Allow configuration of MatrixRTC timers when calling joinRoomSession() ([#4510](https://github.com/matrix-org/matrix-js-sdk/pull/4510)). Contributed by @hughns.
* When state says you've left ongoing call, rejoin ([#4342](https://github.com/matrix-org/matrix-js-sdk/pull/4342)). Contributed by @AndrewFerr.
* Remove redundant type arguments in function call ([#4507](https://github.com/matrix-org/matrix-js-sdk/pull/4507)). Contributed by @AndrewFerr.
* MatrixRTCSession: handle rate limit errors ([#4494](https://github.com/matrix-org/matrix-js-sdk/pull/4494)). Contributed by @AndrewFerr.
* Send/receive error details with widgets ([#4492](https://github.com/matrix-org/matrix-js-sdk/pull/4492)). Contributed by @AndrewFerr.
* Capture HTTP error response headers \& handle Retry-After header (MSC4041) ([#4471](https://github.com/matrix-org/matrix-js-sdk/pull/4471)). Contributed by @AndrewFerr.
* Add RoomWidgetClient.sendToDeviceViaWidgetApi() ([#4475](https://github.com/matrix-org/matrix-js-sdk/pull/4475)). Contributed by @hughns.
Changes in [34.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.1) (2024-11-12)
====================================================================================================
# Security
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
Changes in [34.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.0) (2024-11-12)
====================================================================================================
# Security
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
====================================================================================================
## 🦖 Deprecations
* Deprecate `CreateSecretStorageOpts.keyBackupInfo` used in `CryptoApi.bootstrapSecretStorage.` ([#4474](https://github.com/matrix-org/matrix-js-sdk/pull/4474)). Contributed by @florianduros.
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
* Remove abandoned MSC3886, MSC3903, MSC3906 experimental implementations ([#4469](https://github.com/matrix-org/matrix-js-sdk/pull/4469)). Contributed by @t3chguy.
* Deprecate `MatrixClient.getDehydratedDevice` ([#4467](https://github.com/matrix-org/matrix-js-sdk/pull/4467)). Contributed by @florianduros.
* Deprecate top level crypto events re-export ([#4444](https://github.com/matrix-org/matrix-js-sdk/pull/4444)). Contributed by @florianduros.
## ✨ Features
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
* Do not rotate MatrixRTC media encryption key when a new member joins a session ([#4472](https://github.com/matrix-org/matrix-js-sdk/pull/4472)). Contributed by @hughns.
* Avoid `<sender>|<session>` notation in log messages ([#4473](https://github.com/matrix-org/matrix-js-sdk/pull/4473)). Contributed by @richvdh.
* Refactor/simplify Promises in MatrixRTCSession ([#4466](https://github.com/matrix-org/matrix-js-sdk/pull/4466)). Contributed by @AndrewFerr.
* Prepare delayed call leave events more reliably ([#4447](https://github.com/matrix-org/matrix-js-sdk/pull/4447)). Contributed by @AndrewFerr.
## 🐛 Bug Fixes
* Fix DelayedEventInfo type ([#4446](https://github.com/matrix-org/matrix-js-sdk/pull/4446)). Contributed by @AndrewFerr.
Changes in [34.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.9.0) (2024-10-22)
==================================================================================================
## 🦖 Deprecations
* Deprecate the crypto events which are not used by the rust-crypto ([#4442](https://github.com/matrix-org/matrix-js-sdk/pull/4442)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix the rust crypto import in esm environments. ([#4445](https://github.com/matrix-org/matrix-js-sdk/pull/4445)). Contributed by @saul-jb.
* Fix MatrixRTC sender key wrapping ([#4441](https://github.com/matrix-org/matrix-js-sdk/pull/4441)). Contributed by @hughns.
Changes in [34.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.8.0) (2024-10-15)
==================================================================================================
This release removes insecure functionality, resolving CVE-2024-47080 / GHSA-4jf8-g8wp-cx7c.
Changes in [34.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.7.0) (2024-10-08)
==================================================================================================
## 🦖 Deprecations
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() ([#4427](https://github.com/matrix-org/matrix-js-sdk/pull/4427)). Contributed by @hughns.
## ✨ Features
* Bump matrix-rust-sdk to 9.1.0 ([#4435](https://github.com/matrix-org/matrix-js-sdk/pull/4435)). Contributed by @richvdh.
* Rotate Matrix RTC media encryption key when a new member joins a call for Post Compromise Security ([#4422](https://github.com/matrix-org/matrix-js-sdk/pull/4422)). Contributed by @hughns.
* Update media event content types to include captions ([#4403](https://github.com/matrix-org/matrix-js-sdk/pull/4403)). Contributed by @tulir.
* Update OIDC registration types to match latest MSC2966 state ([#4432](https://github.com/matrix-org/matrix-js-sdk/pull/4432)). Contributed by @t3chguy.
* Add `CryptoApi.pinCurrentUserIdentity` and `UserIdentity.needsUserApproval` ([#4415](https://github.com/matrix-org/matrix-js-sdk/pull/4415)). Contributed by @richvdh.
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
==================================================================================================
## 🦖 Deprecations
* Element-R: Mark unsupported MatrixClient methods as deprecated ([#4389](https://github.com/matrix-org/matrix-js-sdk/pull/4389)). Contributed by @richvdh.
## ✨ Features
* Add crypto mode setting for invisible crypto, and apply it to decrypting events ([#4407](https://github.com/matrix-org/matrix-js-sdk/pull/4407)). Contributed by @uhoreg.
* Don't share full key history for RTC per-participant encryption ([#4406](https://github.com/matrix-org/matrix-js-sdk/pull/4406)). Contributed by @hughns.
* Export membership types ([#4405](https://github.com/matrix-org/matrix-js-sdk/pull/4405)). Contributed by @Johennes.
* Fix sending redacts in embedded (widget) mode ([#4398](https://github.com/matrix-org/matrix-js-sdk/pull/4398)). Contributed by @toger5.
* Expose the event ID of a call membership ([#4395](https://github.com/matrix-org/matrix-js-sdk/pull/4395)). Contributed by @robintown.
* MSC4133 - Extended profiles ([#4391](https://github.com/matrix-org/matrix-js-sdk/pull/4391)). Contributed by @Half-Shot.
Changes in [34.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.5.0) (2024-09-10)
==================================================================================================
## 🦖 Deprecations
+1
View File
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
## Examples
+17
View File
@@ -31,5 +31,22 @@ module.exports = {
"@babel/plugin-transform-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
[
"search-and-replace",
{
// Since rewriteImportExtensions doesn't work on dynamic imports (yet), we need to manually replace
// the dynamic rust-crypto import.
// (see https://github.com/babel/babel/issues/16750)
rules:
process.env.NODE_ENV !== "test"
? [
{
search: "./rust-crypto/index.ts",
replace: "./rust-crypto/index.js",
},
]
: [],
},
],
],
};
+16 -11
View File
@@ -1,9 +1,15 @@
import clc from "cli-color";
import fs from "fs";
import readline from "readline";
import sdk, { ClientEvent, EventType, MsgType, RoomEvent } from "matrix-js-sdk";
import { KnownMembership } from "matrix-js-sdk/lib/@types/membership.js";
var myHomeServer = "http://localhost:8008";
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
baseUrl: myHomeServer,
accessToken: myAccessToken,
userId: myUserId,
});
@@ -15,7 +21,6 @@ var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -97,7 +102,7 @@ rl.on("line", function (line) {
})
.then(function (url) {
var content = {
msgtype: "m.file",
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
@@ -138,7 +143,7 @@ rl.on("line", function (line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("sync", function (state, prevState, data) {
matrixClient.on(ClientEvent.Sync, function (state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
@@ -149,7 +154,7 @@ matrixClient.on("sync", function (state, prevState, data) {
}
});
matrixClient.on("Room", function () {
matrixClient.on(ClientEvent.Room, function () {
setRoomList();
if (!viewingRoom) {
printRoomList();
@@ -158,7 +163,7 @@ matrixClient.on("Room", function () {
});
// print incoming messages.
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
@@ -305,7 +310,7 @@ function printRoomInfo(room) {
print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-"));
eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") {
if (eventType === EventType.RoomMember) {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType);
@@ -343,7 +348,7 @@ function printLine(event) {
name = name.slice(0, maxNameWidth - 1) + "\u2026";
}
if (event.getType() === "m.room.message") {
if (event.getType() === EventType.RoomMessage) {
body = event.getContent().body;
} else if (event.isState()) {
var stateName = event.getType();
@@ -394,4 +399,4 @@ function fixWidth(str, len) {
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
+2 -1
View File
@@ -5,8 +5,9 @@
"main": "app.js",
"author": "",
"license": "Apache 2.0",
"type": "module",
"dependencies": {
"cli-color": "^1.0.0",
"matrix-js-sdk": "^32.0.0"
"matrix-js-sdk": "^34.5.0"
}
}
+14 -11
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.5.0",
"version": "34.12.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
@@ -58,12 +58,12 @@
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.8.2",
"matrix-widget-api": "^1.10.0",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "10"
"uuid": "11"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -81,6 +81,7 @@
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
@@ -88,22 +89,24 @@
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.0.0",
"babel-plugin-search-and-replace": "^1.1.1",
"debug": "^4.3.4",
"eslint": "8.57.0",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.1",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"husky": "^9.0.0",
"jest": "^29.0.0",
@@ -121,7 +124,7 @@
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.3.3"
"typescript": "^5.4.2"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# For the published and dist versions of the package,
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
# needing to build before linting or testing.
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
-1
View File
@@ -1 +0,0 @@
switch_package_to_release.cjs
+1 -1
View File
@@ -66,7 +66,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
+4 -4
View File
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
const masterKey = await encryptAESSecretStorageItem(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
const selfSigningKey = await encryptAESSecretStorageItem(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
const userSigningKey = await encryptAESSecretStorageItem(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
+163 -18
View File
@@ -83,12 +83,14 @@ import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
DeviceIsolationMode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmAccount,
createOlmSession,
@@ -103,6 +105,7 @@ 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";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -133,7 +136,7 @@ async function expectSendRoomKey(
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const Olm = global.Olm;
const Olm = globalThis.Olm;
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
@@ -216,7 +219,7 @@ async function expectSendMegolmMessage(
}
describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, initCrypto: InitCrypto) => {
if (!global.Olm) {
if (!globalThis.Olm) {
// currently we use libolm to implement the crypto in the tests, so need it to be present.
logger.warn("not running megolm tests: Olm not present");
return;
@@ -227,7 +230,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
const Olm = global.Olm;
const Olm = globalThis.Olm;
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = "";
@@ -746,6 +749,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
describe("IsolationMode decryption tests", () => {
newBackendOnly(
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
},
);
newBackendOnly(
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
},
);
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
// This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto
// mode.
//
// This test is almost the same as the "Alice receives a megolm
// message" test, with the main difference that we set the
// crypto mode.
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode
aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
return await testUtils.awaitDecryption(event);
}
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1805,13 +1897,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
const p2pSession = new globalThis.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
"readonly",
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
const account = new globalThis.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
@@ -1953,13 +2045,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
const p2pSession = new globalThis.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
"readonly",
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
const account = new globalThis.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
@@ -3046,11 +3138,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Import a new key that should be uploaded
const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<IKeyBackup>((resolve) => {
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
resolve(uploadPayload);
return {
status: 200,
@@ -3179,15 +3271,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
});
describe("Check if the cross signing keys are available for a user", () => {
describe("User identity", () => {
let keyResponder: E2EKeyResponder;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
@@ -3203,6 +3293,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.getCrypto()!
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are available for a tracked user", async () => {
@@ -3213,11 +3309,60 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are not available for an unknown user", async () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
newBackendOnly("An unverified user changes identity", async () => {
// We have to be tracking Bob's keys, which means we need to share a room with him
syncResponder.sendOrQueueSyncResponse({
...getSyncResponse([BOB_TEST_USER_ID]),
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
// Bob changes his cross-signing keys
keyResponder.addCrossSigningData(testData.BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA);
syncResponder.sendOrQueueSyncResponse({
next_batch: "2",
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(true);
}
// Pinning the new identity should clear the needsUserApproval flag.
await aliceClient.getCrypto()!.pinCurrentUserIdentity(BOB_TEST_USER_ID);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.needsUserApproval).toBe(false);
}
});
});
@@ -3354,7 +3499,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
return client;
}
function mkEncryptionEvent(content: Object) {
function mkEncryptionEvent(content: object) {
return mkEventCustom({
sender: persistentStoreClient.getSafeUserId(),
type: "m.room.encryption",
@@ -3367,7 +3512,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
*
* @param stateEvents - Additional state events for the test room
*/
function getSyncResponseWithState(stateEvents: Array<Object>) {
function getSyncResponseWithState(stateEvents: Array<object>) {
const roomResponse = {
state: {
events: [
+2 -2
View File
@@ -42,10 +42,10 @@ import {
} from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -91,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
+4 -4
View File
@@ -85,15 +85,15 @@ export function bootstrapCrossSigningTestOlmAccount(
deviceId: string,
keyBackupInfo: KeyBackupInfo[] = [],
): Partial<IDownloadKeyResult> {
const olmAliceMSK = new global.Olm.PkSigning();
const olmAliceMSK = new globalThis.Olm.PkSigning();
const masterPrivkey = olmAliceMSK.generate_seed();
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
const olmAliceUSK = new global.Olm.PkSigning();
const olmAliceUSK = new globalThis.Olm.PkSigning();
const userPrivkey = olmAliceUSK.generate_seed();
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
const olmAliceSSK = new global.Olm.PkSigning();
const olmAliceSSK = new globalThis.Olm.PkSigning();
const sskPrivkey = olmAliceSSK.generate_seed();
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
@@ -181,7 +181,7 @@ export async function createOlmSession(
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
const session = new globalThis.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
@@ -0,0 +1,152 @@
/*
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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { createClient, MatrixClient } from "../../../src";
import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
/**
* Integration tests for to-device messages functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: testData.TEST_USER_ID,
accessToken: "akjgkrgjsalice",
deviceId: testData.TEST_DEVICE_ID,
});
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
new E2EKeyReceiver(homeserverUrl);
const syncResponder = new SyncResponder(homeserverUrl);
// add bob as known user
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
fetchMock.post(
new URL(
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
homeserverUrl,
).toString(),
{ filter_id: "fid" },
);
await initCrypto(aliceClient);
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
aliceClient.stopClient();
fetchMock.mockReset();
});
describe("encryptToDeviceMessages", () => {
it("returns empty batch for device that is not known", async () => {
await aliceClient.startClient();
const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);
expect(toDeviceBatch).toBeDefined();
const { batch, eventType } = toDeviceBatch!;
expect(eventType).toBe("m.room.encrypted");
expect(batch.length).toBe(0);
});
it("returns encrypted batch for known device", async () => {
await aliceClient.startClient();
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
one_time_keys: testData.BOB_ONE_TIME_KEYS,
}));
await syncPromise(aliceClient);
const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);
expect(toDeviceBatch?.batch.length).toBe(1);
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
expect(userId).toBe(testData.BOB_TEST_USER_ID);
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
expect(payload.sender_key).toEqual(expect.any(String));
expect(payload.ciphertext).toEqual(
expect.objectContaining({
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
body: expect.any(String),
type: 0,
},
}),
);
// for future: check that bob's device can decrypt the ciphertext?
});
});
});
+4 -3
View File
@@ -90,11 +90,12 @@ jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
beforeAll(async () => {
// we use the libolm primitives in the test, so init the Olm library
await global.Olm.init();
await globalThis.Olm.init();
});
// load the rust library. This can take a few seconds on a slow GH worker.
beforeAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
await RustSdkCryptoJs.initAsync();
}, 10000);
@@ -263,7 +264,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new global.Olm.SAS();
const olmSAS = new globalThis.Olm.SAS();
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
// alice responds with a 'key' ...
@@ -357,7 +358,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new global.Olm.SAS();
const olmSAS = new globalThis.Olm.SAS();
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
+1 -1
View File
@@ -67,7 +67,7 @@ function getSyncResponse(roomMembers: string[]) {
}
describe("DeviceList management:", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("not running deviceList tests: Olm not present");
return;
}
+2 -2
View File
@@ -80,7 +80,7 @@ describe("MatrixClient opts", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@@ -135,7 +135,7 @@ describe("MatrixClient opts", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
+4 -4
View File
@@ -577,7 +577,7 @@ describe("MatrixClient syncing", () => {
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
const member = client!.getRoom(roomOne)!.getMember(userC)!;
expect(member.name).toEqual("The Boss");
expect(member.getAvatarUrl("home.server.url", 1, 1, "", false, false)).toBeTruthy();
expect(member.getAvatarUrl("https://home.server.url", 1, 1, "", false, false)).toBeTruthy();
});
});
@@ -2406,7 +2406,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests"),
cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"),
});
const idbHttpBackend = idbTestClient.httpBackend;
const idbClient = idbTestClient.client;
@@ -2486,7 +2486,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
indexedDB: globalThis.indexedDB,
dbName: "test",
}),
});
@@ -2558,7 +2558,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
indexedDB: globalThis.indexedDB,
dbName: "test",
}),
});
@@ -266,8 +266,8 @@ describe("MSC4108SignInWithQR", () => {
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
});
@@ -319,8 +319,8 @@ describe("MSC4108SignInWithQR", () => {
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
});
+1 -1
View File
@@ -19,7 +19,7 @@ import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
// eslint-disable-next-line @typescript-eslint/no-require-imports
globalThis.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
+2 -2
View File
@@ -27,7 +27,7 @@ export class AccountDataAccumulator {
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
* Will be used by `sendSyncResponseWithUpdatedAccountData`
*/
public accountDataEvents: Map<String, any> = new Map();
public accountDataEvents: Map<string, any> = new Map();
/**
* Intercept requests to set a particular type of account data.
@@ -101,7 +101,7 @@ export class AccountDataAccumulator {
})),
},
});
} catch (err) {
} catch {
// Might fail with "Cannot queue more than one /sync response" if called too often.
// It's ok if it fails here, the sync response is cumulative and will contain
// the latest account data.
+15 -5
View File
@@ -101,9 +101,8 @@ export const makeGeolocationPosition = ({
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
}): GeolocationPosition => {
const { toJSON, ...coordsJSON } = {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
@@ -112,5 +111,16 @@ export const makeGeolocationPosition = ({
heading: null,
speed: null,
...coords,
},
});
};
const posJSON = {
timestamp: timestamp ?? 1647256791840,
coords: {
toJSON: () => coordsJSON,
...coordsJSON,
},
};
return {
toJSON: () => posJSON,
...posJSON,
};
};
@@ -66,6 +66,7 @@ BOB_DATA = {
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"DoYouSpeakWhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
result = f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
if alt_master_key is not None:
result += f"""
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
};
"""
def build_cross_signing_keys_data(user_data) -> dict:
return result
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
+47
View File
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
}
}
}
}
};
+2 -2
View File
@@ -127,7 +127,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
} catch {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
@@ -560,7 +560,7 @@ export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
if (globalThis.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
}
+13 -10
View File
@@ -147,10 +147,13 @@ export class MockRTCPeerConnection {
}
constructor() {
this.localDescription = {
const localDescriptionJSON = {
sdp: DUMMY_SDP,
type: "offer",
toJSON: function () {},
type: "offer" as RTCSdpType,
};
this.localDescription = {
toJSON: () => localDescriptionJSON,
...localDescriptionJSON,
};
this.readyToNegotiate = new Promise<void>((resolve) => {
@@ -265,7 +268,7 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
}
export class MockMediaStreamTrack {
@@ -579,11 +582,11 @@ export class MockCallFeed {
}
export function installWebRTCMocks() {
global.navigator = {
globalThis.navigator = {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
global.window = {
globalThis.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
@@ -593,13 +596,13 @@ export function installWebRTCMocks() {
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
globalThis.document = {};
// @ts-ignore Mock
global.AudioContext = MockAudioContext;
globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock
global.RTCRtpReceiver = {
globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
@@ -607,7 +610,7 @@ export function installWebRTCMocks() {
};
// @ts-ignore Mock
global.RTCRtpSender = {
globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
+1 -1
View File
@@ -9,7 +9,7 @@ import { defer } from "../../src/utils";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
let shouldFailSendToDevice: Boolean;
let shouldFailSendToDevice: boolean;
let onSendToDeviceFailure: () => void;
let onSendToDeviceSuccess: () => void;
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
+2 -2
View File
@@ -22,12 +22,12 @@ import { AutoDiscovery } from "../../src/autodiscovery";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
const realAutoDiscoveryFetch: typeof globalThis.fetch = AutoDiscovery.fetchFn;
describe("AutoDiscovery", function () {
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof globalThis.fetch);
return httpBackend;
};
+4 -4
View File
@@ -29,8 +29,8 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
// eslint-disable-next-line no-global-assign
Buffer = undefined;
global.atob = NodeBuffer.atob;
global.btoa = NodeBuffer.btoa;
globalThis.atob = NodeBuffer.atob;
globalThis.btoa = NodeBuffer.btoa;
}
});
@@ -38,9 +38,9 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
// eslint-disable-next-line no-global-assign
Buffer = origBuffer;
// @ts-ignore
global.atob = undefined;
globalThis.atob = undefined;
// @ts-ignore
global.btoa = undefined;
globalThis.btoa = undefined;
});
it("Should decode properly encoded data", () => {
@@ -0,0 +1,38 @@
/*
* 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 { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";
describe("key-passphrase", () => {
describe("keyFromAuthData", () => {
it("should throw an error if salt or iterations are missing", async () => {
// missing salt
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
// missing iterations
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
});
it("should derive key from auth data", async () => {
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
expect(key).toBeDefined();
});
});
});
+2 -2
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
+25 -14
View File
@@ -63,20 +63,6 @@ describe("ContentRepo", function () {
);
});
it("should put fragments from mxc:// URIs after any query parameters", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
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(
@@ -98,5 +84,30 @@ describe("ContentRepo", function () {
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
it("should drop mxc urls with invalid server_name", () => {
const mxcUri = "mxc://server.name:test/foobar";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls with invalid media_id", () => {
const mxcUri = "mxc://server.name/foobar:test";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls attempting a path traversal attack", () => {
const mxcUri = "mxc://../../../../foo";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls attempting to pass query parameters", () => {
const mxcUri = "mxc://server.name/foobar?bar=baz";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls with too many parts", () => {
const mxcUri = "mxc://server.name/foo//bar";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
});
});
+44
View File
@@ -0,0 +1,44 @@
/*
* 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 { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";
describe("recovery key", () => {
describe("decodeRecoveryKey", () => {
it("should thrown an incorrect length error", () => {
const key = [0, 1];
const encodedKey = encodeRecoveryKey(key)!;
expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
});
it("should thrown an incorrect parity", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
let encodedKey = encodeRecoveryKey(key)!;
// Mutate the encoded key to have incorrect parity
encodedKey = encodedKey.replace("EsSz", "EsSZ");
expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
});
it("should decode a valid encoded key", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
const encodedKey = encodeRecoveryKey(key)!;
expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
});
});
});
+114 -2
View File
@@ -26,8 +26,9 @@ import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
import { KnownMembership } from "../../src/@types/membership";
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
const Olm = global.Olm;
const Olm = globalThis.Olm;
function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
return new Promise((resolve) => {
@@ -607,7 +608,7 @@ describe("Crypto", function () {
event.claimedEd25519Key = null;
try {
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
} catch {
// we expect this to fail because we don't have the
// decryption keys yet
}
@@ -1245,6 +1246,117 @@ describe("Crypto", function () {
});
});
describe("encryptToDeviceMessages", () => {
let client: TestClient;
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
const payload = { hello: "world" };
let encryptedPayload: object;
let crypto: Crypto;
beforeEach(async () => {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) };
});
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
};
crypto = client.client.getCrypto() as Crypto;
});
afterEach(async () => {
ensureOlmSessionsForDevices.mockRestore();
encryptMessageForDevice.mockRestore();
await client.stop();
});
it("returns encrypted batch where devices known", async () => {
const deviceInfoMap: DeviceInfoMap = new Map([
[
"@bob:example.org",
new Map([
["bobweb", new DeviceInfo("bobweb")],
["bobmobile", new DeviceInfo("bobmobile")],
]),
],
["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])],
]);
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap);
// const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);
const batch = await client.client.getCrypto()?.encryptToDeviceMessages(
"m.test.type",
[
{ userId: "@bob:example.org", deviceId: "bobweb" },
{ userId: "@bob:example.org", deviceId: "bobmobile" },
{ userId: "@carol:example.org", deviceId: "caroldesktop" },
{ userId: "@carol:example.org", deviceId: "carolmobile" }, // not known
],
payload,
);
expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith(
["@bob:example.org", "@carol:example.org"],
false,
);
expect(encryptMessageForDevice).toHaveBeenCalledTimes(3);
const expectedPayload = expect.objectContaining({
...encryptedPayload,
"org.matrix.msgid": expect.any(String),
"sender_key": expect.any(String),
});
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch.length).toEqual(3);
expect(batch).toEqual({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
userId: "@bob:example.org",
deviceId: "bobweb",
payload: expectedPayload,
},
{
userId: "@bob:example.org",
deviceId: "bobmobile",
payload: expectedPayload,
},
{
userId: "@carol:example.org",
deviceId: "caroldesktop",
payload: expectedPayload,
},
]),
});
});
it("returns empty batch if no devices known", async () => {
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map());
const batch = await crypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
],
payload,
);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch).toEqual([]);
});
});
describe("checkSecretStoragePrivateKey", () => {
let client: TestClient;
+4 -4
View File
@@ -44,13 +44,13 @@ badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should throw if no callback is provided", async () => {
@@ -80,7 +80,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
expect(pubKey).toEqual(masterKeyPub);
// check that the pkSigning object corresponds to the pubKey
const signature = pkSigning.sign("message");
const util = new global.Olm.Utility();
const util = new globalThis.Olm.Utility();
try {
util.ed25519_verify(pubKey, "message", signature);
} finally {
@@ -199,7 +199,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
[
"MemoryCryptoStore",
+3 -3
View File
@@ -43,10 +43,10 @@ const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2
const ROOM_ID = "!ROOM:ID";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("MegolmDecryption", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
@@ -101,7 +101,7 @@ describe("MegolmDecryption", function () {
describe("receives some keys:", function () {
let groupSession: OutboundGroupSession;
beforeEach(async function () {
groupSession = new global.Olm.OutboundGroupSession();
groupSession = new globalThis.Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
+2 -2
View File
@@ -47,13 +47,13 @@ function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
}
describe("OlmDevice", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
let aliceOlmDevice: OlmDevice;
+5 -5
View File
@@ -33,7 +33,7 @@ import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm;
const Olm = globalThis.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -155,7 +155,7 @@ function makeTestClient(cryptoStore: CryptoStore) {
}
describe("MegolmBackup", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
@@ -205,14 +205,14 @@ describe("MegolmBackup", function () {
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
const realSetTimeout = globalThis.setTimeout;
jest.spyOn(globalThis, "setTimeout").mockImplementation(function (f, n) {
return realSetTimeout(f!, n! / 100);
});
});
afterEach(function () {
jest.spyOn(global, "setTimeout").mockRestore();
jest.spyOn(globalThis, "setTimeout").mockRestore();
});
test("fail if crypto not enabled", async () => {
+23 -25
View File
@@ -84,13 +84,13 @@ async function makeTestClient(
}
describe("Cross Signing", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should sign the master key with the device key", async function () {
@@ -140,9 +140,7 @@ describe("Cross Signing", function () {
});
}
const error = new MatrixError(errorResponse);
error.httpStatus == 401;
throw error;
throw new MatrixError(errorResponse, 401);
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
@@ -371,10 +369,10 @@ describe("Cross Signing", function () {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -490,7 +488,7 @@ describe("Cross Signing", function () {
};
await alice.crypto!.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
const bobOlmAccount = new globalThis.Olm.Account();
bobOlmAccount.create();
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
const bobDeviceUnsigned = {
@@ -624,10 +622,10 @@ describe("Cross Signing", function () {
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -690,10 +688,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
await resetCrossSigningKeys(alice);
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -757,10 +755,10 @@ describe("Cross Signing", function () {
expect(bobDeviceTrust.isTofu()).toBeTruthy();
// Alice downloads new SSK for Bob
const bobMasterSigning2 = new global.Olm.PkSigning();
const bobMasterSigning2 = new globalThis.Olm.PkSigning();
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
const bobSigning2 = new global.Olm.PkSigning();
const bobSigning2 = new globalThis.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2: CrossSigningKeyInfo = {
@@ -827,7 +825,7 @@ describe("Cross Signing", function () {
});
it("should offer to upgrade device verifications to cross-signing", async function () {
let upgradeResolveFunc: Function;
let upgradeResolveFunc: () => void;
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -866,7 +864,7 @@ describe("Cross Signing", function () {
// cross-signing key is signed by his Dynabook, which alice has
// verified, and ask if the device verification should be upgraded to a
// cross-signing verification
let upgradePromise = new Promise((resolve) => {
let upgradePromise = new Promise<void>((resolve) => {
upgradeResolveFunc = resolve;
});
await resetCrossSigningKeys(alice);
@@ -885,7 +883,7 @@ describe("Cross Signing", function () {
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
expect(bobTrust2.isTofu()).toBeTruthy();
upgradePromise = new Promise((resolve) => {
upgradePromise = new Promise<void>((resolve) => {
upgradeResolveFunc = resolve;
});
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
@@ -907,10 +905,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -982,10 +980,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -1042,10 +1040,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -1090,12 +1088,12 @@ describe("Cross Signing", function () {
});
describe("userHasCrossSigningKeys", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
return;
}
beforeAll(() => {
return global.Olm.init();
return globalThis.Olm.init();
});
let aliceClient: MatrixClient;
+1 -1
View File
@@ -32,7 +32,7 @@ export async function resetCrossSigningKeys(
}
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
const decryption = new global.Olm.PkDecryption();
const decryption = new globalThis.Olm.PkDecryption();
decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
decryption.free();
+3 -3
View File
@@ -19,16 +19,16 @@ import { TestClient } from "../../TestClient";
import { logger } from "../../../src/logger";
import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("Dehydration", () => {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running dehydration unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should rehydrate a dehydrated device", async () => {
@@ -51,7 +51,7 @@ const requests = [
];
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function (name, dbFactory) {
+6 -6
View File
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
@@ -69,20 +69,20 @@ function sign<T extends IObject | ICurve25519AuthData>(
}
describe("Secrets", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should store and retrieve a secret", async function () {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const signing = new global.Olm.PkSigning();
const signing = new globalThis.Olm.PkSigning();
const signingKey = signing.generate_seed();
const signingPubKey = signing.init_with_seed(signingKey);
@@ -332,7 +332,7 @@ describe("Secrets", function () {
});
it("bootstraps when cross-signing keys in secret storage", async function () {
const decryption = new global.Olm.PkDecryption();
const decryption = new globalThis.Olm.PkDecryption();
const storagePrivateKey = decryption.get_private_key();
const bob: MatrixClient = await makeTestClient(
@@ -612,7 +612,7 @@ describe("Secrets", function () {
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
key_id: await encryptAESSecretStorageItem(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id,
"m.megolm_backup.v1",
+1 -1
View File
@@ -20,7 +20,7 @@ import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from
import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("CryptoStore tests for %s", function (name, dbFactory) {
@@ -27,21 +27,21 @@ describe("IndexedDBCryptoStore", () => {
it("Should be true if there is a legacy database", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(true);
});
it("Should be true if there is a legacy database in non migrated state", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
await store.setMigrationState(MigrationState.NOT_STARTED);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(true);
});
@@ -54,18 +54,18 @@ describe("IndexedDBCryptoStore", () => {
])("Exists and Migration state is %s", (migrationState) => {
it("Should be false if migration has started", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
await store.setMigrationState(migrationState);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(false);
});
});
it("Should be false if there is no legacy database", async () => {
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(false);
});
@@ -17,10 +17,10 @@ limitations under the License.
import "../../../olm-loader";
import { logger } from "../../../../src/logger";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("QR code verification", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification tests: libolm not present");
return;
}
@@ -20,12 +20,12 @@ import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients } from "./util";
const Olm = global.Olm;
const Olm = globalThis.Olm;
jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
+3 -3
View File
@@ -29,13 +29,13 @@ import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
const Olm = globalThis.Olm;
let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
@@ -454,7 +454,7 @@ describe("SAS verification", function () {
let bobSasEvent: ISasEvent | null;
let aliceVerifier: SAS;
let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function;
let clearTestClientTimeouts: () => void;
beforeEach(async function () {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@@ -34,7 +34,7 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("triggers a request for key sharing upon completion", async () => {
+3 -3
View File
@@ -29,12 +29,12 @@ describe("sha256", () => {
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = global.crypto;
const oldCrypto = globalThis.crypto;
try {
global.crypto = {} as any;
globalThis.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
global.crypto = oldCrypto;
globalThis.crypto = oldCrypto;
}
});
});
+112 -3
View File
@@ -30,12 +30,13 @@ import {
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
WidgetApiResponseError,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
@@ -90,7 +91,11 @@ class MockWidgetApi extends EventEmitter {
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public transport = { reply: jest.fn() };
public transport = {
reply: jest.fn(),
send: jest.fn(),
sendComplete: jest.fn(),
};
}
declare module "../../src/types" {
@@ -187,6 +192,46 @@ describe("RoomWidgetClient", () => {
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
widgetApi.transport.send.mockRejectedValue(error);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_BAD_JSON",
error: "Invalid body",
};
const widgetError = new WidgetApiResponseError("failed to send", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.send.mockRejectedValue(widgetError);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError);
});
});
describe("delayed events", () => {
@@ -493,6 +538,23 @@ describe("RoomWidgetClient", () => {
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
};
const encryptedContentMap = new Map<string, Map<string, object>>([
["@alice:example.org", new Map([["aliceMobile", { hello: "alice!" }]])],
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
]);
it("sends unencrypted (sendToDeviceViaWidgetApi)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi(
"org.example.foo",
false,
unencryptedContentMap,
);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends unencrypted (sendToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
@@ -534,6 +596,17 @@ describe("RoomWidgetClient", () => {
});
});
it("sends encrypted (sendToDeviceViaWidgetApi)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi("org.example.foo", true, encryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
"@alice:example.org": { aliceMobile: { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
});
});
it.each([
{ encrypted: false, title: "unencrypted" },
{ encrypted: true, title: "encrypted" },
@@ -570,6 +643,42 @@ describe("RoomWidgetClient", () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_UNKNOWN",
error: "Bad request",
};
const widgetError = new WidgetApiResponseError("failed to get token", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
});
});
it("gets TURN servers", async () => {
+189
View File
@@ -0,0 +1,189 @@
/*
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 { MatrixError } from "../../../src";
type IErrorJson = MatrixError["data"];
describe("MatrixError", () => {
let headers: Headers;
beforeEach(() => {
headers = new Headers({ "Content-Type": "application/json" });
});
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
return new MatrixError(data, httpStatus, url, undefined, headers);
}
it("should accept absent retry time from rate-limit error", () => {
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(null);
});
it("should retrieve retry_after_ms from rate-limit error", () => {
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(150000);
});
it("should ignore retry_after_ms if errcode is not M_LIMIT_EXCEEDED", () => {
const err = makeMatrixError(429, { errcode: "M_UNKNOWN", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(null);
});
it("should retrieve numeric Retry-After header from rate-limit error", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should retrieve Date Retry-After header from rate-limit error", () => {
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
expect(err.getRetryAfterMs()).toEqual(60000);
});
it("should prefer M_FORBIDDEN errcode over HTTP status code 429", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(429, { errcode: "M_FORBIDDEN" });
expect(err.isRateLimitError()).toBe(false);
// retrieve Retry-After header even for non-M_LIMIT_EXCEEDED errors
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should prefer M_LIMIT_EXCEEDED errcode over HTTP status code 400", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(400, { errcode: "M_LIMIT_EXCEEDED" });
expect(err.isRateLimitError()).toBe(true);
// retrieve Retry-After header even for non-429 errors
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should reject invalid Retry-After header", () => {
for (const invalidValue of ["-1", "1.23", new Date(0).toString()]) {
headers.set("Retry-After", invalidValue);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(() => err.getRetryAfterMs()).toThrow(
"value is not a valid HTTP-date or non-negative decimal integer",
);
}
});
it("should reject too-large Retry-After header", () => {
headers.set("Retry-After", "1" + Array(500).fill("0").join(""));
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
});
describe("can be converted to data compatible with the widget api", () => {
it("from default values", () => {
const matrixError = new MatrixError();
const widgetApiErrorData = {
http_status: 400,
http_headers: {},
url: "",
response: {
errcode: "M_UNKNOWN",
error: "Unknown message",
},
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
it("from non-default values", () => {
headers.set("Retry-After", "120");
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const matrixError = makeMatrixError(statusCode, data, url);
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
});
describe("can be created from data received from the widget api", () => {
it("from minimal data", () => {
const statusCode = 400;
const data = {
errcode: "M_UNKNOWN",
error: "Something went wrong.",
};
const url = "";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {},
url,
response: data,
};
headers.delete("Content-Type");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
it("from more data", () => {
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
headers.set("Retry-After", "120");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
});
});
+3 -3
View File
@@ -60,11 +60,11 @@ describe("FetchHttpApi", () => {
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
globalThis.fetch = jest.fn();
expect(globalThis.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
expect(globalThis.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
+9 -5
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 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.
@@ -41,12 +41,13 @@ describe("MatrixHttpApi", () => {
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
getAllResponseHeaders: jest.fn(),
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
globalThis.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
@@ -59,7 +60,7 @@ describe("MatrixHttpApi", () => {
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined!;
globalThis.XMLHttpRequest = undefined!;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
@@ -171,7 +172,10 @@ describe("MatrixHttpApi", () => {
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
mocked(xhr.getResponseHeader).mockImplementation((name) =>
name.toLowerCase() === "content-type" ? "application/json" : null,
);
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
+107 -35
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 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.
@@ -86,13 +86,28 @@ describe("anySignal", () => {
});
describe("parseErrorResponse", () => {
let headers: Headers;
const xhrHeaderMethods = {
getResponseHeader: (name: string) => headers.get(name),
getAllResponseHeaders: () => {
let allHeaders = "";
headers.forEach((value, key) => {
allHeaders += `${key.toLowerCase()}: ${value}\r\n`;
});
return allHeaders;
},
};
beforeEach(() => {
headers = new Headers();
});
it("should resolve Matrix Errors from XHR", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -108,14 +123,11 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from fetch", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -131,13 +143,12 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from XHR with urls", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
responseURL: "https://example.com",
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -154,15 +165,12 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from fetch with urls", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
url: "https://example.com",
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -178,6 +186,66 @@ describe("parseErrorResponse", () => {
);
});
describe("with HTTP headers", () => {
function addHeaders(headers: Headers) {
headers.set("Age", "0");
headers.set("Date", "Thu, 01 Jan 1970 00:00:00 GMT"); // value contains colons
headers.set("x-empty", "");
headers.set("x-multi", "1");
headers.append("x-multi", "2");
}
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
}
it("should resolve HTTP Errors from XHR with headers", () => {
headers.set("Content-Type", "text/plain");
addHeaders(headers);
const err = parseErrorResponse({
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest) as HTTPError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve HTTP Errors from fetch with headers", () => {
headers.set("Content-Type", "text/plain");
addHeaders(headers);
const err = parseErrorResponse({
headers,
status: 500,
} as Response) as HTTPError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve Matrix Errors from XHR with headers", () => {
headers.set("Content-Type", "application/json");
addHeaders(headers);
const err = parseErrorResponse(
{
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
) as MatrixError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve Matrix Errors from fetch with headers", () => {
headers.set("Content-Type", "application/json");
addHeaders(headers);
const err = parseErrorResponse(
{
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
) as MatrixError;
compareHeaders(headers, err.httpHeaders);
});
});
it("should set a sensible default error message on MatrixError", () => {
let err = new MatrixError();
expect(err.message).toEqual("MatrixError: Unknown message");
@@ -188,14 +256,11 @@ describe("parseErrorResponse", () => {
});
it("should handle no type gracefully", () => {
// No Content-Type header
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -203,31 +268,38 @@ describe("parseErrorResponse", () => {
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
});
it("should handle invalid type gracefully", () => {
it("should handle empty type gracefully", () => {
headers.set("Content-Type", " ");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
).toStrictEqual(new Error("Error parsing Content-Type '': TypeError: argument string is required"));
});
it("should handle plaintext errors", () => {
it("should handle invalid type gracefully", () => {
headers.set("Content-Type", "unknown");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new Error("Error parsing Content-Type 'unknown': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
headers.set("Content-Type", "text/plain");
expect(
parseErrorResponse(
{
headers,
status: 418,
} as Response,
"I'm a teapot",
+129 -11
View File
@@ -102,7 +102,7 @@ type HttpLookup = {
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
thenCall?: Function;
thenCall?: () => void;
};
interface Options extends ICreateRoomOpts {
@@ -403,7 +403,7 @@ describe("MatrixClient", function () {
async function assertRequestsMade(
responses: {
prefix?: string;
error?: { httpStatus: Number; errcode: string };
error?: { httpStatus: number; errcode: string };
data?: { event_id: string };
}[],
expectRejects = false,
@@ -1029,6 +1029,124 @@ describe("MatrixClient", function () {
});
});
describe("extended profiles", () => {
const unstableMSC4133Prefix = `${ClientPrefix.Unstable}/uk.tcpip.msc4133`;
const userId = "@profile_user:example.org";
beforeEach(() => {
unstableFeatures["uk.tcpip.msc4133"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["uk.tcpip.msc4133"] = false;
const errorMessage = "Server does not support extended profiles";
await expect(client.doesServerSupportExtendedProfiles()).resolves.toEqual(false);
await expect(client.getExtendedProfile(userId)).rejects.toThrow(errorMessage);
await expect(client.getExtendedProfileProperty(userId, "test_key")).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfileProperty("test_key", "foo")).rejects.toThrow(errorMessage);
await expect(client.deleteExtendedProfileProperty("test_key")).rejects.toThrow(errorMessage);
await expect(client.patchExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
});
it("can fetch a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId),
data: testProfile,
},
];
await expect(client.getExtendedProfile(userId)).resolves.toEqual(testProfile);
expect(httpLookups).toHaveLength(0);
});
it("can fetch a property from a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId) + "/test_key",
data: testProfile,
},
];
await expect(client.getExtendedProfileProperty(userId, "test_key")).resolves.toEqual("foo");
expect(httpLookups).toHaveLength(0);
});
it("can set a property in our extended profile", async () => {
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
expectBody: {
test_key: "foo",
},
},
];
await expect(client.setExtendedProfileProperty("test_key", "foo")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can delete a property in our extended profile", async () => {
httpLookups = [
{
method: "DELETE",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
},
];
await expect(client.deleteExtendedProfileProperty("test_key")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can patch our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
const patchedProfile = {
existing: "key",
test_key: "foo",
};
httpLookups = [
{
method: "PATCH",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: patchedProfile,
expectBody: testProfile,
},
];
await expect(client.patchExtendedProfile(testProfile)).resolves.toEqual(patchedProfile);
});
it("can replace our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: testProfile,
expectBody: testProfile,
},
];
await expect(client.setExtendedProfile(testProfile)).resolves.toEqual(undefined);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -1397,7 +1515,7 @@ describe("MatrixClient", function () {
});
describe("emitted sync events", function () {
function syncChecker(expectedStates: [string, string | null][], done: Function) {
function syncChecker(expectedStates: [string, string | null][], done: () => void) {
return function syncListener(state: SyncState, old: SyncState | null) {
const expected = expectedStates.shift();
logger.log("'sync' curr=%s old=%s EXPECT=%s", state, old, expected);
@@ -1419,7 +1537,7 @@ describe("MatrixClient", function () {
it("should transition null -> PREPARED after the first /sync", async () => {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1436,7 +1554,7 @@ describe("MatrixClient", function () {
error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1476,7 +1594,7 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1487,7 +1605,7 @@ describe("MatrixClient", function () {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1512,7 +1630,7 @@ describe("MatrixClient", function () {
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1531,7 +1649,7 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1546,7 +1664,7 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1577,7 +1695,7 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
File diff suppressed because it is too large Load Diff
+4 -55
View File
@@ -107,7 +107,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -120,7 +120,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(2);
});
@@ -133,7 +133,7 @@ describe("MSC3089TreeSpace", () => {
});
client.invite = fn;
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -155,61 +155,10 @@ describe("MSC3089TreeSpace", () => {
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
];
await tree.invite(target, true, false);
await tree.invite(target, true);
expect(fn).toHaveBeenCalledTimes(4);
});
it("should share keys with invitees", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
// Mock the history check as best as possible
const historyVis = "shared";
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
// We're not expecting a super rigid test: the function that calls this internally isn't
// really being tested here.
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
// Note: inverse test is implicit from other tests, which disable the call stack of this
// test in order to pass.
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(1);
expect(historyFn).toHaveBeenCalledTimes(1);
});
it("should not share keys with invitees if inappropriate history visibility", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
const historyVis = "joined"; // NOTE: Changed.
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(0);
expect(historyFn).toHaveBeenCalledTimes(1);
});
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
makePowerLevels(pls);
const fn = jest
+3 -3
View File
@@ -70,7 +70,7 @@ describe("Beacon", () => {
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
@@ -108,11 +108,11 @@ describe("Beacon", () => {
);
// back to 'now'
jest.spyOn(global.Date, "now").mockReturnValue(now);
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("creates beacon from event", () => {
+1 -1
View File
@@ -56,7 +56,7 @@ describe("oidc authorization", () => {
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
global.TextEncoder = TextEncoder;
globalThis.TextEncoder = TextEncoder;
});
beforeEach(() => {
+1 -1
View File
@@ -35,7 +35,7 @@ describe("Pushers", () => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
});
});
+2 -2
View File
@@ -82,7 +82,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store,
});
});
@@ -256,7 +256,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store,
});
});
+1 -1
View File
@@ -54,7 +54,7 @@ describe("Read receipt", () => {
userId: "@user:server",
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
});
client.isGuest = () => false;
client.supportsThreads = () => true;
+2 -2
View File
@@ -53,8 +53,8 @@ describe("realtime-callbacks", function () {
it("should set 'this' to the global object", function () {
let passed = false;
const callback = function (this: typeof global) {
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
const callback = function (this: typeof globalThis) {
expect(this).toBe(globalThis); // eslint-disable-line @typescript-eslint/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
passed = true;
};
-171
View File
@@ -1,171 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../olm-loader";
import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/base64";
import { DummyTransport } from "./DummyTransport";
function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
}
describe("ECDHv2", function () {
beforeAll(async function () {
await global.Olm.init();
});
describe("with crypto", () => {
it("initiator wants to sign in", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await alice.send(message);
const bobReceive = await bob.receive();
expect(bobReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("initiator wants to reciprocate", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await bob.send(message);
const aliceReceive = await alice.receive();
expect(aliceReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("double connect", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
await expect(alice.connect()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("closed", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
alice.close();
await expect(alice.connect()).rejects.toThrow();
await expect(alice.send({})).rejects.toThrow();
await expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("require ciphertext", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
await expect(bob.receive()).rejects.toThrow();
});
it("ciphertext before set up", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
await alice.cancel(RendezvousFailureReason.Unknown);
await expect(alice.receive()).rejects.toThrow();
});
});
});
-712
View File
@@ -1,712 +0,0 @@
/*
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
import "../../olm-loader";
import {
MSC3906Rendezvous,
RendezvousCode,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousIntent,
} from "../../../src/rendezvous";
import {
ECDHv2RendezvousCode as ECDHRendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { Device, MatrixClient } from "../../../src";
import {
MSC3886SimpleHttpRendezvousTransport,
MSC3886SimpleHttpRendezvousTransportDetails,
} from "../../../src/rendezvous/transports";
import { DummyTransport } from "./DummyTransport";
import { decodeBase64 } from "../../../src/base64";
import { logger } from "../../../src/logger";
import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api";
type UserID = string;
type DeviceID = string;
type Fingerprint = string;
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;
function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device {
return {
deviceId,
userId,
getFingerprint: () => fingerprint,
} as unknown as Device;
}
function mockDeviceMap(
userId: UserID,
deviceId: DeviceID,
deviceKey?: Fingerprint,
otherDevices: SimpleDeviceMap = {},
): Map<string, Map<string, Device>> {
const deviceMap: Map<string, Map<string, Device>> = new Map();
const myDevices: Map<string, Device> = new Map();
if (deviceKey) {
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
}
deviceMap.set(userId, myDevices);
for (const u in otherDevices) {
let userDevices = deviceMap.get(u);
if (!userDevices) {
userDevices = new Map();
deviceMap.set(u, userDevices);
}
for (const d in otherDevices[u]) {
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
}
}
return deviceMap;
}
function makeMockClient(opts: {
userId: UserID;
deviceId: DeviceID;
deviceKey?: Fingerprint;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: SimpleDeviceMap;
verificationFunction?: (
userId: string,
deviceId: string,
verified: boolean,
blocked: boolean,
known: boolean,
) => void;
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
}): [MatrixClient, Map<string, Map<string, Device>>] {
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
return [
{
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
if (feature === "org.matrix.msc3886") {
return opts.msc3886Enabled;
} else if (feature === "org.matrix.msc3882") {
return opts.getLoginTokenEnabled;
} else {
return false;
}
}),
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
},
getCachedCapabilities() {
return opts.msc3882r0Only
? {}
: {
capabilities: {
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
};
},
getUserId() {
return opts.userId;
},
getSafeUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
baseUrl: "https://example.com",
getCrypto() {
return {
getUserDeviceInfo(
[userId]: string[],
downloadUncached?: boolean,
): Promise<Map<string, Map<string, Device>>> {
return Promise.resolve(deviceMap);
},
getCrossSigningKeyId(key: CrossSigningKey): string | null {
return opts.crossSigningIds?.[key] ?? null;
},
setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise<void> {
return Promise.resolve();
},
crossSignDevice(deviceId: string): Promise<void> {
return Promise.resolve();
},
getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
return Promise.resolve({
ed25519: opts.deviceKey!,
curve25519: "aaaa",
});
},
};
},
} as unknown as MatrixClient,
deviceMap,
];
}
function makeTransport(name: string, uri = "https://test.rz/123456") {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "http.v1", uri });
}
describe("Rendezvous", function () {
beforeAll(async function () {
await global.Olm.init();
});
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
const userId: UserID = "@user:example.com";
beforeEach(function () {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
transports = [];
});
afterEach(function () {
transports.forEach((x) => x.cleanup());
});
it("generate and cancel", async function () {
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
client: alice,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
const codePromise = aliceRz.generateCode();
await httpBackend.flush("");
await aliceRz.generateCode();
expect(typeof aliceRz.code).toBe("string");
await codePromise;
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
"https://fallbackserver/rz/123",
);
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
await httpBackend.flush("");
await expect(cancelPromise).resolves.toBeUndefined();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequests();
await aliceRz.close();
});
async function testNoProtocols({
getLoginTokenEnabled,
msc3882r0Only,
}: {
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.finish",
outcome: "unsupported",
});
})();
await aliceStartProm;
await bobStartPromise;
}
it("no protocols - r0", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
});
it("no protocols - stable", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
});
it("new device declines protocol with outcome unsupported", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.finish", outcome: "unsupported" });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("new device requests an invalid protocol", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "bad protocol" });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("decline on existing device", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
await aliceRz.declineLoginOnExistingDevice();
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.finish", outcome: "declined" });
});
it("approve on existing device + no verification", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobCompleteProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
await bobEcdh.send({ type: "m.login.finish", outcome: "success" });
})();
await confirmProm;
await bobCompleteProm;
});
async function completeLogin(devices: SimpleDeviceMap) {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const aliceVerification = jest.fn();
const [alice, deviceMap] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
deviceKey: "aaaa",
verificationFunction: aliceVerification,
crossSigningIds: {
master: "mmmmm",
},
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobLoginProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
await bobEcdh.send({ type: "m.login.finish", outcome: "success", device_id: "BOB", device_key: "bbbb" });
})();
expect(await confirmProm).toEqual("BOB");
await bobLoginProm;
return {
aliceTransport,
aliceEcdh,
aliceRz,
bobTransport,
bobEcdh,
deviceMap,
};
}
it("approve on existing device + verification", async function () {
const { bobEcdh, aliceRz } = await completeLogin({
[userId]: {
BOB: "bbbb",
},
});
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
const bobVerifyProm = (async () => {
const verified = await bobEcdh.receive();
expect(verified).toEqual({
type: "m.login.finish",
outcome: "verified",
verifying_device_id: "ALICE",
verifying_device_key: "aaaa",
master_key: "mmmmm",
});
})();
await verifyProm;
await bobVerifyProm;
});
it("device not online within timeout", async function () {
const { aliceRz } = await completeLogin({});
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("device appears online within timeout", async function () {
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears before the timeout
setTimeout(() => {
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1000);
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
});
it("device appears online after timeout", async function () {
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears after the timeout
setTimeout(() => {
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1500);
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("mismatched device key", async function () {
const { aliceRz } = await completeLogin({
[userId]: {
BOB: "XXXX",
},
});
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
});
});
@@ -1,453 +0,0 @@
/*
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
import type { MatrixClient } from "../../../src";
import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
function makeMockClient(opts: { userId: string; deviceId: string; msc3886Enabled: boolean }): MatrixClient {
return {
doesServerSupportUnstableFeature(feature: string) {
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
},
getUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
requestLoginToken() {
return Promise.resolve({ login_token: "token" });
},
baseUrl: "https://example.com",
} as unknown as MatrixClient;
}
describe("SimpleHttpRendezvousTransport", function () {
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
beforeEach(function () {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
});
async function postAndCheckLocation(
msc3886Enabled: boolean,
fallbackRzServer: string,
locationResponse: string,
expectedFinalLocation: string,
) {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
{
// initial POST
const expectedPostLocation = msc3886Enabled
? `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous`
: fallbackRzServer;
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", expectedPostLocation).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: locationResponse,
},
},
};
await httpBackend.flush("");
await prom;
}
const details = await simpleHttpTransport.details();
expect(details.uri).toBe(expectedFinalLocation);
{
// first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", expectedFinalLocation).response = {
body: {},
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({});
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
}
}
it("should throw an error when no server available", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
});
it("POST to fallback server", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
});
it("POST with no location", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {},
},
};
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
});
it("POST with absolute path response", async function () {
await postAndCheckLocation(false, "https://fallbackserver/rz", "/123", "https://fallbackserver/123");
});
it("POST to built-in MSC3886 implementation", async function () {
await postAndCheckLocation(
true,
"https://fallbackserver/rz",
"123",
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
);
});
it("POST with relative path response including parent", async function () {
await postAndCheckLocation(
false,
"https://fallbackserver/rz/abc",
"../xyz/123",
"https://fallbackserver/rz/xyz/123",
);
});
it("POST to follow 307 to other server", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 307,
headers: {
location: "https://redirected.fallbackserver/rz",
},
},
};
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://redirected.fallbackserver/rz/123",
etag: "aaa",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
});
it("POST and GET", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
}
{
// first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "aaa",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({ foo: "baa" });
}
{
// subsequent GET which should have etag from previous request
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-none-match"]).toEqual("aaa");
}).response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "bbb",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({ foo: "baa" });
}
});
it("POST and PUTs", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
{
// first PUT without etag
const prom = simpleHttpTransport.send({ a: "b" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
expect(headers["if-match"]).toBeUndefined();
expect(data).toEqual({ a: "b" });
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
etag: "aaa",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
{
// subsequent PUT which should have etag from previous request
const prom = simpleHttpTransport.send({ c: "d" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-match"]).toEqual("aaa");
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
etag: "bbb",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
});
it("POST and DELETE", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// Create
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
}
{
// Cancel
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
await httpBackend.flush("");
await prom;
}
});
it("details before ready", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await expect(simpleHttpTransport.details()).rejects.toThrow();
});
it("send after cancelled", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
});
it("receive before ready", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await expect(simpleHttpTransport.receive()).rejects.toThrow();
});
it("404 failure callback", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 404,
headers: {},
},
};
await Promise.all([
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
httpBackend.flush("", 1),
]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
});
it("404 failure callback mapped to expired", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
},
},
};
await httpBackend.flush("");
await prom;
}
{
// GET with 404 to simulate expiry
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 404,
headers: {},
},
};
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
}
});
});
@@ -61,7 +61,7 @@ describe("OutgoingRequestProcessor", () => {
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
onlyData: true,
});
@@ -208,7 +208,7 @@ describe("OutgoingRequestProcessor", () => {
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
// also create a UIA callback
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
@@ -458,7 +458,7 @@ describe("OutgoingRequestProcessor", () => {
throw new Error("Failed to fetch");
}
});
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
@@ -26,7 +26,6 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
import * as TestData from "../../test-utils/test-data";
import {
ConnectionError,
CryptoEvent,
HttpApiEvent,
HttpApiEventHandlerMap,
IHttpOpts,
@@ -37,6 +36,7 @@ import {
import * as testData from "../../test-utils/test-data";
import { BackupDecryptor } from "../../../src/common-crypto/CryptoBackend";
import { KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
describe("PerSessionKeyBackupDownloader", () => {
/** The downloader under test */
+112 -9
View File
@@ -17,6 +17,7 @@
*/
import {
CollectStrategy,
Curve25519PublicKey,
Ed25519PublicKey,
HistoryVisibility as RustHistoryVisibility,
@@ -31,6 +32,7 @@ import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { defer } from "../../../src/utils";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { KnownMembership } from "../../../src/@types/membership";
import { DeviceIsolationMode, AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "../../../src/crypto-api";
describe("RoomEncryptor", () => {
describe("History Visibility", () => {
@@ -99,7 +101,7 @@ describe("RoomEncryptor", () => {
getEncryptionTargetMembers: jest.fn().mockReturnValue([mockRoomMember]),
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(true),
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Invited),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(null),
} as unknown as Mocked<Room>;
roomEncryptor = new RoomEncryptor(
@@ -111,6 +113,8 @@ describe("RoomEncryptor", () => {
);
});
const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
it("should ensure that there is only one shareRoomKey at a time", async () => {
const deferredShare = defer<void>();
const insideOlmShareRoom = defer<void>();
@@ -120,19 +124,19 @@ describe("RoomEncryptor", () => {
await deferredShare.promise;
});
roomEncryptor.prepareForEncryption(false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
await insideOlmShareRoom.promise;
// call several times more
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("World"), false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false, defaultDevicesIsolationMode);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("World"), false, defaultDevicesIsolationMode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(1);
deferredShare.resolve();
await roomEncryptor.prepareForEncryption(false);
await roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
// should have been called again
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(6);
@@ -158,8 +162,16 @@ describe("RoomEncryptor", () => {
let firstMessageFinished: string | null = null;
const firstRequest = roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
const secondRequest = roomEncryptor.encryptEvent(createMockEvent("Edit of Hello"), false);
const firstRequest = roomEncryptor.encryptEvent(
createMockEvent("Hello"),
false,
defaultDevicesIsolationMode,
);
const secondRequest = roomEncryptor.encryptEvent(
createMockEvent("Edit of Hello"),
false,
defaultDevicesIsolationMode,
);
firstRequest.then(() => {
if (firstMessageFinished === null) {
@@ -181,5 +193,96 @@ describe("RoomEncryptor", () => {
expect(firstMessageFinished).toBe("hello");
});
describe("DeviceIsolationMode", () => {
type TestCase = [
string,
{
mode: DeviceIsolationMode;
expectedStrategy: CollectStrategy;
globalBlacklistUnverifiedDevices: boolean;
},
];
const testCases: TestCase[] = [
[
"Share AllDevicesIsolationMode",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, false),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - with blacklist unverified",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, false),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned - blacklist true",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - Verified user problems true",
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, true),
globalBlacklistUnverifiedDevices: false,
},
],
[
'Share AllDevicesIsolationMode - with blacklist unverified - Verified user problems true"',
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, true),
globalBlacklistUnverifiedDevices: true,
},
],
];
let capturedSettings: CollectStrategy | undefined = undefined;
beforeEach(() => {
capturedSettings = undefined;
mockOlmMachine.shareRoomKey.mockImplementationOnce(async (roomId, users, encryptionSettings) => {
capturedSettings = encryptionSettings.sharingStrategy;
});
});
it.each(testCases)(
"prepareForEncryption should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.prepareForEncryption(globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
it.each(testCases)(
"encryptEvent should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.encryptEvent(createMockEvent("Hello"), globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
});
});
});
+5 -6
View File
@@ -2,13 +2,12 @@ import { Mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { CryptoEvent, HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
import { CryptoEvent, KeyBackupSession } from "../../../src/crypto-api/index.ts";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import * as testData from "../../test-utils/test-data";
import * as TestData from "../../test-utils/test-data";
import { IKeyBackup } from "../../../src/crypto/backup";
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
import { RustBackupManager } from "../../../src/rust-crypto/backup";
import { RustBackupManager, KeyBackup } from "../../../src/rust-crypto/backup";
describe("Upload keys to backup", () => {
/** The backup manager under test */
@@ -26,7 +25,7 @@ describe("Upload keys to backup", () => {
let idGenerator = 0;
function mockBackupRequest(keyCount: number): RustSdkCryptoJs.KeysBackupRequest {
const requestBody: IKeyBackup = {
const requestBody: KeyBackup = {
rooms: {
"!room1:server": {
sessions: {},
@@ -34,7 +33,7 @@ describe("Upload keys to backup", () => {
},
};
for (let i = 0; i < keyCount; i++) {
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as IKeyBackupSession;
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as KeyBackupSession;
}
return {
id: "id" + idGenerator++,
+162 -5
View File
@@ -30,7 +30,6 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import {
CryptoEvent,
Device,
DeviceVerification,
encodeBase64,
@@ -69,8 +68,9 @@ import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
@@ -425,7 +425,7 @@ describe("initRustCrypto", () => {
}, 10000);
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
}
@@ -1362,13 +1362,52 @@ describe("RustCrypto", () => {
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
olmMachine.getIdentity.mockResolvedValue({
free: jest.fn(),
isVerified: jest.fn().mockReturnValue(true),
wasPreviouslyVerified: jest.fn().mockReturnValue(true),
});
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy();
});
});
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
@@ -1554,6 +1593,124 @@ describe("RustCrypto", () => {
await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle));
});
});
describe("encryptToDeviceMessages", () => {
let rustCrypto: RustCrypto;
let testOlmMachine: RustSdkCryptoJs.OlmMachine;
beforeEach(async () => {
testOlmMachine = await OlmMachine.initialize(
new RustSdkCryptoJs.UserId(testData.TEST_USER_ID),
new RustSdkCryptoJs.DeviceId(testData.TEST_DEVICE_ID),
);
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
rustCrypto = await makeTestRustCrypto();
expect(OlmMachine.initFromStore).toHaveBeenCalled();
});
afterEach(() => {
testOlmMachine?.free();
});
const payload = { hello: "world" };
it("returns empty batch if devices not known", async () => {
const getMissingSessions = jest.spyOn(testOlmMachine, "getMissingSessions");
const getDevice = jest.spyOn(testOlmMachine, "getDevice");
const batch = await rustCrypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
],
payload,
);
expect(getMissingSessions.mock.calls[0][0].length).toBe(2);
expect(getDevice).toHaveBeenCalledTimes(3);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch).toEqual([]);
});
it("returns encrypted batch for known devices", async () => {
// Make m aware of another device, and get some OTK to be able to establish a session.
await testOlmMachine.markRequestAsSent(
"foo",
RustSdkCryptoJs.RequestType.KeysQuery,
JSON.stringify({
device_keys: {
"@example:localhost": {
AFGUOBTZWM: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "AFGUOBTZWM",
keys: {
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec",
},
signatures: {
"@example:localhost": {
"ed25519:AFGUOBTZWM":
"RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA",
},
},
user_id: "@example:localhost",
unsigned: {
device_display_name: "rust-sdk",
},
},
},
},
failures: {},
}),
);
await testOlmMachine.markRequestAsSent(
"bar",
RustSdkCryptoJs.RequestType.KeysClaim,
JSON.stringify({
one_time_keys: {
"@example:localhost": {
AFGUOBTZWM: {
"signed_curve25519:AAAABQ": {
key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
signatures: {
"@example:localhost": {
"ed25519:AFGUOBTZWM":
"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg",
},
},
},
},
},
},
failures: {},
}),
);
const batch = await rustCrypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
{ deviceId: "AFGUOBTZWM", userId: "@example:localhost" },
],
payload,
);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch.length).toEqual(1);
expect(batch?.batch[0].deviceId).toEqual("AFGUOBTZWM");
expect(batch?.batch[0].userId).toEqual("@example:localhost");
expect(batch?.batch[0].payload).toEqual(
expect.objectContaining({
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"ciphertext": expect.any(Object),
"org.matrix.msgid": expect.any(String),
"sender_key": expect.any(String),
}),
);
});
});
});
/** Build a MatrixHttpApi instance */
+1 -1
View File
@@ -98,7 +98,7 @@ describe("secret-storage", () => {
} else if (secretName == "secretE") {
return { aaaa: {}, bbbb: {} };
} else {
null;
return null;
}
}),
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
+1 -1
View File
@@ -12,7 +12,7 @@ jest.useFakeTimers();
describe("MatrixScheduler", function () {
let scheduler: MatrixScheduler<Record<string, boolean>>;
let retryFn: Function | null;
let retryFn: ((event: MatrixEvent | null, attempt: number, err: MatrixError) => number) | null;
let queueFn: ((event: MatrixEvent) => string | null) | null;
let deferred: IDeferred<Record<string, boolean>>;
const roomId = "!foo:bar";
+1 -1
View File
@@ -18,6 +18,7 @@ import { Mocked } from "jest-mock";
import {
AccountDataClient,
calculateKeyCheck,
PassphraseInfo,
SecretStorageCallbacks,
SecretStorageKeyDescriptionAesV1,
@@ -25,7 +26,6 @@ import {
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { calculateKeyCheck } from "../../src/crypto/aes";
import { randomString } from "../../src/randomstring";
describe("ServerSideSecretStorageImpl", function () {
+3 -3
View File
@@ -794,7 +794,7 @@ describe("SyncAccumulator", function () {
}
afterEach(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("should copy summary properties", function () {
@@ -851,11 +851,11 @@ describe("SyncAccumulator", function () {
const delta = 1000;
const startingTs = 1000;
jest.spyOn(global.Date, "now").mockReturnValue(startingTs);
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs);
sa.accumulate(RES_WITH_AGE);
jest.spyOn(global.Date, "now").mockReturnValue(startingTs + delta);
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs + delta);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual(
+19 -19
View File
@@ -119,9 +119,9 @@ describe("Call", function () {
const errorListener = () => {};
beforeEach(function () {
prevNavigator = global.navigator;
prevDocument = global.document;
prevWindow = global.window;
prevNavigator = globalThis.navigator;
prevDocument = globalThis.document;
prevWindow = globalThis.window;
installWebRTCMocks();
@@ -159,9 +159,9 @@ describe("Call", function () {
call.hangup(CallErrorCode.UserHangup, true);
client.stop();
global.navigator = prevNavigator;
global.window = prevWindow;
global.document = prevDocument;
globalThis.navigator = prevNavigator;
globalThis.window = prevWindow;
globalThis.document = prevDocument;
jest.useRealTimers();
});
@@ -788,17 +788,17 @@ describe("Call", function () {
});
it("should return false if window or document are undefined", () => {
global.window = undefined!;
globalThis.window = undefined!;
expect(supportsMatrixCall()).toBe(false);
global.window = prevWindow;
global.document = undefined!;
globalThis.window = prevWindow;
globalThis.document = undefined!;
expect(supportsMatrixCall()).toBe(false);
});
it("should return false if RTCPeerConnection throws", () => {
// @ts-ignore - writing to window as we are simulating browser edge-cases
global.window = {};
Object.defineProperty(global.window, "RTCPeerConnection", {
globalThis.window = {};
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
get: () => {
throw Error("Secure mode, naaah!");
},
@@ -810,11 +810,11 @@ describe("Call", function () {
"should return false if RTCPeerConnection & RTCSessionDescription " +
"& RTCIceCandidate & mediaDevices are unavailable",
() => {
global.window.RTCPeerConnection = undefined!;
global.window.RTCSessionDescription = undefined!;
global.window.RTCIceCandidate = undefined!;
globalThis.window.RTCPeerConnection = undefined!;
globalThis.window.RTCSessionDescription = undefined!;
globalThis.window.RTCIceCandidate = undefined!;
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
global.navigator.mediaDevices = undefined;
globalThis.navigator.mediaDevices = undefined;
expect(supportsMatrixCall()).toBe(false);
},
);
@@ -1228,7 +1228,7 @@ describe("Call", function () {
});
describe("Screen sharing", () => {
const waitNegotiateFunc = (resolve: Function): void => {
const waitNegotiateFunc = (resolve: () => void): void => {
mockSendEvent.mockImplementationOnce(() => {
// Note that the peer connection here is a dummy one and always returns
// dummy SDP, so there's not much point returning the content: the SDP will
@@ -1305,7 +1305,7 @@ describe("Call", function () {
});
it("removes RTX codec from screen sharing transcievers", async () => {
mocked(global.RTCRtpSender.getCapabilities).mockReturnValue({
mocked(globalThis.RTCRtpSender.getCapabilities).mockReturnValue({
codecs: [
{ mimeType: "video/rtx", clockRate: 90000 },
{ mimeType: "video/somethingelse", clockRate: 90000 },
@@ -1816,8 +1816,8 @@ describe("Call", function () {
it("should emit IceFailed error on the successor call if RTCPeerConnection throws", async () => {
// @ts-ignore - writing to window as we are simulating browser edge-cases
global.window = {};
Object.defineProperty(global.window, "RTCPeerConnection", {
globalThis.window = {};
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
get: () => {
throw Error("Secure mode, naaah!");
},
+1 -1
View File
@@ -101,7 +101,7 @@ describe("CallFeed", () => {
it.each([
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
])("should react to call state, when !isLocal()", (state: CallState, expected: boolean) => {
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
expect(feed.connected).toBe(expected);
+1 -1
View File
@@ -31,7 +31,7 @@ describe("Media Handler", function () {
beforeEach(() => {
mockMediaDevices = new MockMediaDevices();
global.navigator = {
globalThis.navigator = {
mediaDevices: mockMediaDevices.typed(),
} as unknown as Navigator;
@@ -126,7 +126,7 @@ describe("GroupCallStats", () => {
it("doing nothing if process already running", async () => {
// @ts-ignore
jest.spyOn(global, "setInterval").mockReturnValue(22);
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
stats.start();
expect(setInterval).toHaveBeenCalledTimes(1);
stats.start();
@@ -146,8 +146,8 @@ describe("GroupCallStats", () => {
});
it("finish stats process if was started", async () => {
// @ts-ignore
jest.spyOn(global, "setInterval").mockReturnValue(22);
jest.spyOn(global, "clearInterval");
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
jest.spyOn(globalThis, "clearInterval");
stats.start();
expect(setInterval).toHaveBeenCalledTimes(1);
stats.stop();
@@ -155,7 +155,7 @@ describe("GroupCallStats", () => {
});
it("do nothing if stats process was not started", async () => {
jest.spyOn(global, "clearInterval");
jest.spyOn(globalThis, "clearInterval");
stats.stop();
expect(clearInterval).not.toHaveBeenCalled();
});

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