Compare commits

...

46 Commits

Author SHA1 Message Date
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
RiotRobot c4048d985d v34.10.0 2024-11-05 13:49:13 +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
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
90 changed files with 3140 additions and 4707 deletions
+4 -6
View File
@@ -1,5 +1,5 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc", "node"],
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
@@ -108,10 +108,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
@@ -129,7 +127,7 @@ module.exports = {
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
// Ensure .ts extension on imports outside of tests
"node/file-extension-in-import": [
"n/file-extension-in-import": [
"error",
"always",
{
+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: element-hq/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: element-hq/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: element-hq/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
+4 -4
View File
@@ -12,7 +12,7 @@ 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@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
if: github.event_name != 'merge_group'
@@ -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 -5
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:
@@ -61,26 +61,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
+3 -3
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
@@ -276,7 +276,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 +304,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:
+1 -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:
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["element-hq/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:
+2 -2
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
+5 -5
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
@@ -38,7 +38,7 @@ jobs:
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -55,7 +55,7 @@ jobs:
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -72,7 +72,7 @@ jobs:
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -97,7 +97,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: element-hq/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:
+59
View File
@@ -1,3 +1,62 @@
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
+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",
},
]
: [],
},
],
],
};
+10 -8
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.7.0-rc.0",
"version": "34.10.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.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,23 +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-node": "^11.1.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.3",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"husky": "^9.0.0",
"jest": "^29.0.0",
+2 -2
View File
@@ -3499,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",
@@ -3512,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: [
@@ -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?
});
});
});
+1
View File
@@ -95,6 +95,7 @@ beforeAll(async () => {
// 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);
@@ -267,7 +267,7 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
fn();
// TODO: mock timers properly
return -1 as any;
});
@@ -320,7 +320,7 @@ describe("MSC4108SignInWithQR", () => {
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>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.
+1 -1
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.
+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;
+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("");
});
});
});
+113 -1
View File
@@ -26,6 +26,7 @@ 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;
@@ -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 -6
View File
@@ -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 () => ({});
@@ -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");
+1 -1
View File
@@ -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(
+11 -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,
@@ -1515,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);
@@ -1537,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();
@@ -1554,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();
@@ -1594,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();
@@ -1605,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();
@@ -1630,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();
@@ -1649,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();
@@ -1664,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();
@@ -1695,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
-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);
}
});
});
@@ -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 */
+2 -1
View File
@@ -2,7 +2,8 @@ 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 } 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";
+119 -1
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,
@@ -71,6 +70,7 @@ import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
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";
@@ -1593,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,
@@ -26,7 +27,6 @@ import {
trimTrailingEquals,
} from "../../src/secret-storage";
import { randomString } from "../../src/randomstring";
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
+1 -1
View File
@@ -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
+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);
+3 -5
View File
@@ -111,18 +111,16 @@ type DelayedPartialTimelineEvent = {
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
state_key: string;
transaction_id: string;
};
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
export type DelayedEventInfo = {
delayed_events: DelayedPartialEvent &
delayed_events: (DelayedPartialEvent &
SendDelayedEventResponse &
SendDelayedEventRequestOpts &
{
SendDelayedEventRequestOpts & {
running_since: number;
}[];
})[];
next_batch?: string;
};
+1 -1
View File
@@ -33,7 +33,7 @@ globalThis.__js_sdk_entrypoint = true;
let indexedDB: IDBFactory | undefined;
try {
indexedDB = globalThis.indexedDB;
} catch (e) {}
} catch {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
-34
View File
@@ -1,34 +0,0 @@
/*
* 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.
*/
// string of zeroes, for calculating the key check
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Calculate the MAC for checking the key.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
}
+43 -74
View File
@@ -76,8 +76,8 @@ import {
} from "./http-api/index.ts";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
CryptoEvent as LegacyCryptoEvent,
CryptoEventHandlerMap as LegacyCryptoEventHandlerMap,
fixBackupKey,
ICheckOwnCrossSigningTrustOpts,
ICryptoCallbacks,
@@ -227,6 +227,8 @@ import {
CryptoApi,
decodeRecoveryKey,
ImportRoomKeysOpts,
CryptoEvent,
CryptoEventHandlerMap,
} from "./crypto-api/index.ts";
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
@@ -939,23 +941,25 @@ type RoomStateEvents =
| RoomStateEvent.Update
| RoomStateEvent.Marker;
type CryptoEvents =
| CryptoEvent.KeySignatureUploadFailure
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupFailed
| CryptoEvent.KeyBackupSessionsRemaining
| CryptoEvent.KeyBackupDecryptionKeyCached
| CryptoEvent.RoomKeyRequest
| CryptoEvent.RoomKeyRequestCancellation
| CryptoEvent.VerificationRequest
| CryptoEvent.VerificationRequestReceived
| CryptoEvent.DeviceVerificationChanged
| CryptoEvent.UserTrustStatusChanged
| CryptoEvent.KeysChanged
| CryptoEvent.Warning
| CryptoEvent.DevicesUpdated
| CryptoEvent.WillUpdateDevices
| CryptoEvent.LegacyCryptoStoreMigrationProgress;
type LegacyCryptoEvents =
| LegacyCryptoEvent.KeySignatureUploadFailure
| LegacyCryptoEvent.KeyBackupStatus
| LegacyCryptoEvent.KeyBackupFailed
| LegacyCryptoEvent.KeyBackupSessionsRemaining
| LegacyCryptoEvent.KeyBackupDecryptionKeyCached
| LegacyCryptoEvent.RoomKeyRequest
| LegacyCryptoEvent.RoomKeyRequestCancellation
| LegacyCryptoEvent.VerificationRequest
| LegacyCryptoEvent.VerificationRequestReceived
| LegacyCryptoEvent.DeviceVerificationChanged
| LegacyCryptoEvent.UserTrustStatusChanged
| LegacyCryptoEvent.KeysChanged
| LegacyCryptoEvent.Warning
| LegacyCryptoEvent.DevicesUpdated
| LegacyCryptoEvent.WillUpdateDevices
| LegacyCryptoEvent.LegacyCryptoStoreMigrationProgress;
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
@@ -976,6 +980,7 @@ export type EmittedEvents =
| ClientEvent
| RoomEvents
| RoomStateEvents
| LegacyCryptoEvents
| CryptoEvents
| MatrixEventEvents
| RoomMemberEvents
@@ -1187,6 +1192,7 @@ export type ClientEventHandlerMap = {
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
} & RoomEventHandlerMap &
RoomStateEventHandlerMap &
LegacyCryptoEventHandlerMap &
CryptoEventHandlerMap &
MatrixEventHandlerMap &
RoomMemberEventHandlerMap &
@@ -1671,6 +1677,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Get the current dehydrated device, if any
* @returns A promise of an object containing the dehydrated device
*
* @deprecated MSC2697 device dehydration is not supported for rust cryptography.
*/
public async getDehydratedDevice(): Promise<IDehydratedDevice | undefined> {
try {
@@ -1775,7 +1783,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
try {
indexedDB = global.indexedDB;
if (!indexedDB) return; // No indexedDB support
} catch (e) {
} catch {
// No indexedDB support
return;
}
@@ -2176,16 +2184,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const crypto = new Crypto(this, userId, this.deviceId, this.store, this.cryptoStore, this.verificationMethods!);
this.reEmitter.reEmit(crypto, [
CryptoEvent.KeyBackupFailed,
CryptoEvent.KeyBackupSessionsRemaining,
CryptoEvent.RoomKeyRequest,
CryptoEvent.RoomKeyRequestCancellation,
CryptoEvent.Warning,
CryptoEvent.DevicesUpdated,
CryptoEvent.WillUpdateDevices,
CryptoEvent.DeviceVerificationChanged,
CryptoEvent.UserTrustStatusChanged,
CryptoEvent.KeysChanged,
LegacyCryptoEvent.KeyBackupFailed,
LegacyCryptoEvent.KeyBackupSessionsRemaining,
LegacyCryptoEvent.RoomKeyRequest,
LegacyCryptoEvent.RoomKeyRequestCancellation,
LegacyCryptoEvent.Warning,
LegacyCryptoEvent.DevicesUpdated,
LegacyCryptoEvent.WillUpdateDevices,
LegacyCryptoEvent.DeviceVerificationChanged,
LegacyCryptoEvent.UserTrustStatusChanged,
LegacyCryptoEvent.KeysChanged,
]);
this.logger.debug("Crypto: initialising crypto object...");
@@ -2254,9 +2262,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
// needed.
this.logger.debug("Downloading Rust crypto library");
// blocked on https://github.com/matrix-org/matrix-js-sdk/issues/4392 / https://github.com/babel/babel/issues/16750
// eslint-disable-next-line node/file-extension-in-import
const RustCrypto = await import("./rust-crypto");
const RustCrypto = await import("./rust-crypto/index.ts");
const rustCrypto = await RustCrypto.initRustCrypto({
logger: this.logger,
@@ -2445,7 +2451,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns
*
* @remarks
* Fires {@link CryptoEvent.DeviceVerificationChanged}
* Fires {@link LegacyCryptoEvent.DeviceVerificationChanged}
*
* @deprecated Not supported for Rust Cryptography.
*/
@@ -3254,7 +3260,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
* of the successfully sent messages.
*
* @deprecated Not supported for Rust Cryptography.
* @deprecated Instead use {@link CryptoApi.encryptToDeviceMessages} followed by {@link queueToDevice}.
*/
public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
if (!this.crypto) {
@@ -3644,7 +3650,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
try {
decodeRecoveryKey(recoveryKey);
return true;
} catch (e) {
} catch {
return false;
}
}
@@ -4087,43 +4093,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
await this.http.authedRequest(Method.Delete, path.path, path.queryData, undefined, { prefix: ClientPrefix.V3 });
}
/**
* Share shared-history decryption keys with the given users.
*
* @param roomId - the room for which keys should be shared.
* @param userIds - a list of users to share with. The keys will be sent to
* all of the user's current devices.
*
* @deprecated Do not use this method. It does not work with the Rust crypto stack, and even with the legacy
* stack it introduces a security vulnerability.
*/
public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
const roomEncryption = this.crypto?.getRoomEncryption(roomId);
if (!roomEncryption) {
// unknown room, or unencrypted room
this.logger.error("Unknown room. Not sharing decryption keys");
return;
}
const deviceInfos = await this.crypto.downloadKeys(userIds);
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
for (const [userId, devices] of deviceInfos) {
devicesByUser.set(userId, Array.from(devices.values()));
}
// XXX: Private member access
const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm);
if (alg.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
this.logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
}
/**
* Get the config for the media repository.
* @returns Promise which resolves with an object containing the config.
@@ -7861,7 +7830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
list: determineFeatureSupport(listStable, listUnstable),
fwdPagination: determineFeatureSupport(fwdPaginationStable, fwdPaginationUnstable),
};
} catch (e) {
} catch {
return {
threads: FeatureSupport.None,
list: FeatureSupport.None,
+35 -30
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@@ -14,7 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeParams } from "./utils.ts";
// Validation based on https://spec.matrix.org/v1.12/appendices/#server-name
// We do not use the validation described in https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
// as it'd wrongly make all MXCs invalid due to not allowing `[].:` in server names.
const serverNameRegex =
/^(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:\[[\dA-Fa-f:.]{2,45}])|(?:[A-Za-z\d\-.]{1,255}))(?::\d{1,5})?$/;
function validateServerName(serverName: string): boolean {
const matches = serverNameRegex.exec(serverName);
return matches?.[0] === serverName;
}
// Validation based on https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
const mediaIdRegex = /^[\w-]+$/;
function validateMediaId(mediaId: string): boolean {
const matches = mediaIdRegex.exec(mediaId);
return matches?.[0] === mediaId;
}
/**
* Get the HTTP URL for an MXC URI.
@@ -36,7 +51,7 @@ import { encodeParams } from "./utils.ts";
* for authenticated media will *not* be checked - it is the caller's responsibility
* to do so before calling this function. Note also that `useAuthentication`
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
* @returns The complete URL to the content.
* @returns The complete URL to the content, may be an empty string if the provided mxc is not valid.
*/
export function getHttpUriForMxc(
baseUrl: string,
@@ -51,7 +66,7 @@ export function getHttpUriForMxc(
if (typeof mxc !== "string" || !mxc) {
return "";
}
if (mxc.indexOf("mxc://") !== 0) {
if (!mxc.startsWith("mxc://")) {
if (allowDirectLinks) {
return mxc;
} else {
@@ -59,6 +74,11 @@ export function getHttpUriForMxc(
}
}
const [serverName, mediaId, ...rest] = mxc.slice(6).split("/");
if (rest.length > 0 || !validateServerName(serverName) || !validateMediaId(mediaId)) {
return "";
}
if (useAuthentication) {
allowRedirects = true; // per docs (MSC3916 always expects redirects)
@@ -67,46 +87,31 @@ export function getHttpUriForMxc(
// callers, hopefully.
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix: string;
const isThumbnailRequest = !!width || !!height || !!resizeMethod;
const verb = isThumbnailRequest ? "thumbnail" : "download";
if (useAuthentication) {
prefix = "/_matrix/client/v1/media/download/";
prefix = `/_matrix/client/v1/media/${verb}`;
} else {
prefix = "/_matrix/media/v3/download/";
prefix = `/_matrix/media/v3/${verb}`;
}
const params: Record<string, string> = {};
const url = new URL(`${prefix}/${serverName}/${mediaId}`, baseUrl);
if (width) {
params["width"] = Math.round(width).toString();
url.searchParams.set("width", Math.round(width).toString());
}
if (height) {
params["height"] = Math.round(height).toString();
url.searchParams.set("height", Math.round(height).toString());
}
if (resizeMethod) {
params["method"] = resizeMethod;
}
if (Object.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
if (useAuthentication) {
prefix = "/_matrix/client/v1/media/thumbnail/";
} else {
prefix = "/_matrix/media/v3/thumbnail/";
}
url.searchParams.set("method", resizeMethod);
}
if (typeof allowRedirects === "boolean") {
// We add this after, so we don't convert everything to a thumbnail request.
params["allow_redirect"] = JSON.stringify(allowRedirects);
url.searchParams.set("allow_redirect", JSON.stringify(allowRedirects));
}
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.slice(fragmentOffset);
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
}
const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params);
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
return url.href;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* 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.
*/
/**
* Events emitted by the {@link CryptoApi}
*/
export enum CryptoEvent {
/**
* Fires when the trust status of a user changes.
* The payload is a pair (userId, userTrustLevel). The trust level is one of the values from UserVerificationStatus.
*/
UserTrustStatusChanged = "userTrustStatusChanged",
/**
* Fires when the key backup status changes.
* The payload is a boolean indicating whether the key backup is enabled.
*/
KeyBackupStatus = "crypto.keyBackupStatus",
/**
* Fires when we failed to back up the keys
* The payload is the error code of the error that occurred.
*/
KeyBackupFailed = "crypto.keyBackupFailed",
/**
* Fires when the number of sessions that can be backed up changes.
* The payload is the remaining number of sessions that can be backed up.
*/
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
/**
* Fires when a new valid backup decryption key is in cache.
* This will happen when a secret is received from another session, from secret storage,
* or when a new backup is created from this session.
*
* The payload is the version of the backup for which we have the key for.
*
* This event is only fired by the rust crypto backend.
*/
KeyBackupDecryptionKeyCached = "crypto.keyBackupDecryptionKeyCached",
/**
* Fires when a key verification request is received.
* The payload is a VerificationRequest object representing the request.
*/
VerificationRequestReceived = "crypto.verificationRequestReceived",
/** @deprecated Use {@link DevicesUpdated} instead when using rust crypto */
WillUpdateDevices = "crypto.willUpdateDevices",
/**
* Fires whenever the stored devices for a user have been updated
* The payload is a pair (userIds, initialFetch).
*/
DevicesUpdated = "crypto.devicesUpdated",
/**
* Fires when the user's cross-signing keys have changed or cross-signing
* has been enabled/disabled. The client can use getStoredCrossSigningForUser
* with the user ID of the logged in user to check if cross-signing is
* enabled on the account. If enabled, it can test whether the current key
* is trusted using with checkUserTrust with the user ID of the logged
* in user. The checkOwnCrossSigningTrust function may be used to reconcile
* the trust in the account key.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
* @experimental
*/
KeysChanged = "crossSigning.keysChanged",
/**
* Fires when data is being migrated from legacy crypto to rust crypto.
*
* The payload is a pair `(progress, total)`, where `progress` is the number of steps completed so far, and
* `total` is the total number of steps. When migration is complete, a final instance of the event is emitted, with
* `progress === total === -1`.
*/
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
}
+32
View File
@@ -0,0 +1,32 @@
/*
* 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 { CryptoEvent } from "./CryptoEvent.ts";
import { VerificationRequest } from "./verification.ts";
import { UserVerificationStatus } from "./index.ts";
import { RustBackupCryptoEventMap } from "../rust-crypto/backup.ts";
/**
* A map of the {@link CryptoEvent} fired by the {@link CryptoApi} and their payloads.
*/
export type CryptoEventHandlerMap = {
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void;
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
[CryptoEvent.KeysChanged]: (data: {}) => void;
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
} & RustBackupCryptoEventMap;
+42 -13
View File
@@ -16,6 +16,7 @@ limitations under the License.
import type { SecretsBundle } from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IMegolmSessionData } from "../@types/crypto.ts";
import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
import { Room } from "../models/room.ts";
import { DeviceMap } from "../models/device.ts";
import { UIAuthCallback } from "../interactive-auth.ts";
@@ -25,6 +26,15 @@ import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./keybackup.ts";
import { ISignatures } from "../@types/signed.ts";
import { MatrixEvent } from "../models/event.ts";
/**
* `matrix-js-sdk/lib/crypto-api`: End-to-end encryption support.
*
* The most important type is {@link CryptoApi}, an instance of which can be retrieved via
* {@link MatrixClient.getCrypto}.
*
* @packageDocumentation
*/
/**
* Public interface to the cryptography parts of the js-sdk
*
@@ -181,7 +191,7 @@ export interface CryptoApi {
/**
* Return whether we trust other user's signatures of their devices.
*
* @see {@link Crypto.CryptoApi#setTrustCrossSignedDevices}
* @see {@link CryptoApi.setTrustCrossSignedDevices}
*
* @returns `true` if we trust cross-signed devices, otherwise `false`.
*/
@@ -228,7 +238,7 @@ export interface CryptoApi {
*
* @throws an error if the device is unknown, or has not published any encryption keys.
*
* @remarks Fires {@link CryptoEvent#DeviceVerificationChanged}
* @remarks Fires {@link matrix.CryptoEvent.DeviceVerificationChanged}
*/
setDeviceVerified(userId: string, deviceId: string, verified?: boolean): Promise<void>;
@@ -259,7 +269,7 @@ export interface CryptoApi {
*
* @returns True if cross-signing is ready to be used on this device
*
* @throws May throw {@link ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
* @throws May throw {@link matrix.ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
*/
isCrossSigningReady(): Promise<boolean>;
@@ -327,7 +337,7 @@ export interface CryptoApi {
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and
* whether the private keys are in secret storage.
*
* @throws May throw {@link ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
* @throws May throw {@link matrix.ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
*/
getCrossSigningStatus(): Promise<CrossSigningStatus>;
@@ -355,6 +365,22 @@ export interface CryptoApi {
*/
getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null>;
/**
* Encrypts a given payload object via Olm to-device messages to a given
* set of devices.
*
* @param eventType - the type of the event to send.
* @param devices - an array of devices to encrypt the payload for.
* @param payload - the payload to encrypt.
*
* @returns the batch of encrypted payloads which can then be sent via {@link matrix.MatrixClient#queueToDevice}.
*/
encryptToDeviceMessages(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Device/User verification
@@ -407,8 +433,8 @@ export interface CryptoApi {
*
* If an all-devices verification is already in flight, returns it. Otherwise, initiates a new one.
*
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
* MatrixClient.
* To control the methods offered, set {@link matrix.ICreateClientOpts.verificationMethods} when creating the
* `MatrixClient`.
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
@@ -422,8 +448,8 @@ export interface CryptoApi {
*
* If a verification for this user/device is already in flight, returns it. Otherwise, initiates a new one.
*
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
* MatrixClient.
* To control the methods offered, set {@link matrix.ICreateClientOpts.verificationMethods} when creating the
* `MatrixClient`.
*
* @param userId - ID of the owner of the device to verify
* @param deviceId - ID of the device to verify
@@ -480,7 +506,7 @@ export interface CryptoApi {
/**
* Determine if a key backup can be trusted.
*
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
* @param info - key backup info dict from {@link matrix.MatrixClient.getKeyBackupVersion}.
*/
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
@@ -500,7 +526,7 @@ export interface CryptoApi {
*
* If there are existing backups they will be replaced.
*
* The decryption key will be saved in Secret Storage (the {@link SecretStorageCallbacks.getSecretStorageKey} Crypto
* The decryption key will be saved in Secret Storage (the {@link matrix.SecretStorage.SecretStorageCallbacks.getSecretStorageKey} Crypto
* callback will be called)
* and the backup engine will be started.
*/
@@ -841,9 +867,9 @@ export class DeviceVerificationStatus {
* Check if we should consider this device "verified".
*
* A device is "verified" if either:
* * it has been manually marked as such via {@link MatrixClient#setDeviceVerified}.
* * it has been manually marked as such via {@link matrix.MatrixClient.setDeviceVerified}.
* * it has been cross-signed with a verified signing key, **and** the client has been configured to trust
* cross-signed devices via {@link Crypto.CryptoApi#setTrustCrossSignedDevices}.
* cross-signed devices via {@link CryptoApi.setTrustCrossSignedDevices}.
*
* @returns true if this device is verified via any means.
*/
@@ -948,6 +974,7 @@ export interface CreateSecretStorageOpts {
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
* @deprecated Not used by the Rust crypto stack.
*/
keyBackupInfo?: KeyBackupInfo;
@@ -1071,6 +1098,8 @@ export interface OwnDeviceKeys {
}
export * from "./verification.ts";
export * from "./keybackup.ts";
export type * from "./keybackup.ts";
export * from "./recovery-key.ts";
export * from "./key-passphrase.ts";
export * from "./CryptoEvent.ts";
export type * from "./CryptoEventHandlerMap.ts";
+2 -2
View File
@@ -35,7 +35,7 @@ export interface Aes256AuthData {
* Information about a server-side key backup.
*
* Returned by [`GET /_matrix/client/v3/room_keys/version`](https://spec.matrix.org/v1.7/client-server-api/#get_matrixclientv3room_keysversion)
* and hence {@link MatrixClient#getKeyBackupVersion}.
* and hence {@link matrix.MatrixClient.getKeyBackupVersion}.
*/
export interface KeyBackupInfo {
algorithm: string;
@@ -63,7 +63,7 @@ export interface BackupTrustInfo {
}
/**
* The result of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
*/
export interface KeyBackupCheck {
backupInfo: KeyBackupInfo;
+2 -2
View File
@@ -119,7 +119,7 @@ export interface VerificationRequest
*
* If a verifier has already been created for this request, returns that verifier.
*
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Crypto.Verifier#verify} on the
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Verifier.verify} on the
* returned verifier.
*
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
@@ -281,7 +281,7 @@ export interface Verifier extends TypedEventEmitter<VerifierEvent, VerifierEvent
* Cancel a verification.
*
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
* will reject, and a {@link crypto-api.VerifierEvent.Cancel | VerifierEvent.Cancel} will be emitted.
*
* @param e - the reason for the cancellation.
*/
+2 -2
View File
@@ -521,7 +521,7 @@ export class CrossSigningInfo {
try {
pkVerify(userMaster, uskId, this.userId);
userTrusted = true;
} catch (e) {
} catch {
userTrusted = false;
}
return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse);
@@ -560,7 +560,7 @@ export class CrossSigningInfo {
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
} catch (e) {
} catch {
return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
}
}
+2 -2
View File
@@ -612,7 +612,7 @@ export class OlmDevice {
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
try {
await this.sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
} catch {
// if the session failed to be created, just fall through and
// return an empty result
}
@@ -698,7 +698,7 @@ export class OlmDevice {
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
try {
await this.sessionsInProgress[deviceIdentityKey];
} catch (e) {
} catch {
// if the session failed to be created, then just fall through and
// return an empty result
}
+3 -6
View File
@@ -16,11 +16,8 @@ limitations under the License.
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
// Export for backwards compatibility
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload };
// Export with new names instead of using `as` to not break react-sdk tests
export const encryptAES = encryptAESSecretStorageItem;
export const decryptAES = decryptAESSecretStorageItem;
export { calculateKeyCheck } from "../calculateKeyCheck.ts";
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
export { encryptAESSecretStorageItem as encryptAES, decryptAESSecretStorageItem as decryptAES };
export { calculateKeyCheck } from "../secret-storage.ts";
+3 -3
View File
@@ -1067,7 +1067,7 @@ export class MegolmEncryption extends EncryptionAlgorithm {
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
try {
await this.encryptionPreparation.promise;
} catch (e) {
} catch {
// ignore any errors -- if the preparation failed, we'll just
// restart everything here
}
@@ -2103,7 +2103,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
pendingList.map(async (ev) => {
try {
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
} catch (e) {
} catch {
// don't die if something goes wrong
}
}),
@@ -2128,7 +2128,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
[...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this.crypto);
} catch (e) {
} catch {
// don't die if something goes wrong
}
}),
+1 -1
View File
@@ -43,7 +43,7 @@ import { encodeRecoveryKey } from "../crypto-api/index.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
import { calculateKeyCheck } from "../secret-storage.ts";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
+115 -132
View File
@@ -53,7 +53,7 @@ import { IStore } from "../store/index.ts";
import { Room, RoomEvent } from "../models/room.ts";
import { RoomMember, RoomMemberEvent } from "../models/room-member.ts";
import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event.ts";
import { ToDeviceBatch } from "../models/ToDeviceMessage.ts";
import { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.ts";
import { ClientEvent, IKeysUploadResponse, ISignedKey, IUploadKeySignaturesResponse, MatrixClient } from "../client.ts";
import { IRoomEncryption, RoomList } from "./RoomList.ts";
import { IKeyBackupInfo } from "./keybackup.ts";
@@ -75,6 +75,7 @@ import { MapWithDefault, recursiveMapToObject } from "../utils.ts";
import {
AccountDataClient,
AddSecretStorageKeyOpts,
calculateKeyCheck,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageKeyDescription,
SecretStorageKeyObject,
@@ -99,7 +100,8 @@ import {
KeyBackupCheck,
KeyBackupInfo,
OwnDeviceKeys,
VerificationRequest as CryptoApiVerificationRequest,
CryptoEvent as CryptoApiCryptoEvent,
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
} from "../crypto-api/index.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { deviceInfoToDevice } from "./device-converter.ts";
@@ -109,7 +111,6 @@ import { KnownMembership } from "../@types/membership.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
/* re-exports for backwards compatibility */
export type {
@@ -230,14 +231,18 @@ export interface IMegolmEncryptedContent {
export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent;
export enum CryptoEvent {
/** @deprecated Event not fired by the rust crypto */
DeviceVerificationChanged = "deviceVerificationChanged",
UserTrustStatusChanged = "userTrustStatusChanged",
UserTrustStatusChanged = CryptoApiCryptoEvent.UserTrustStatusChanged,
/** @deprecated Event not fired by the rust crypto */
UserCrossSigningUpdated = "userCrossSigningUpdated",
/** @deprecated Event not fired by the rust crypto */
RoomKeyRequest = "crypto.roomKeyRequest",
/** @deprecated Event not fired by the rust crypto */
RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
KeyBackupStatus = "crypto.keyBackupStatus",
KeyBackupFailed = "crypto.keyBackupFailed",
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
KeyBackupStatus = CryptoApiCryptoEvent.KeyBackupStatus,
KeyBackupFailed = CryptoApiCryptoEvent.KeyBackupFailed,
KeyBackupSessionsRemaining = CryptoApiCryptoEvent.KeyBackupSessionsRemaining,
/**
* Fires when a new valid backup decryption key is in cache.
@@ -248,8 +253,9 @@ export enum CryptoEvent {
*
* This event is only fired by the rust crypto backend.
*/
KeyBackupDecryptionKeyCached = "crypto.keyBackupDecryptionKeyCached",
KeyBackupDecryptionKeyCached = CryptoApiCryptoEvent.KeyBackupDecryptionKeyCached,
/** @deprecated Event not fired by the rust crypto */
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
/** @deprecated Use `VerificationRequestReceived`. */
VerificationRequest = "crypto.verification.request",
@@ -259,12 +265,14 @@ export enum CryptoEvent {
*
* The payload is a {@link Crypto.VerificationRequest}.
*/
VerificationRequestReceived = "crypto.verificationRequestReceived",
VerificationRequestReceived = CryptoApiCryptoEvent.VerificationRequestReceived,
/** @deprecated Event not fired by the rust crypto */
Warning = "crypto.warning",
WillUpdateDevices = "crypto.willUpdateDevices",
DevicesUpdated = "crypto.devicesUpdated",
KeysChanged = "crossSigning.keysChanged",
/** @deprecated Use {@link DevicesUpdated} instead when using rust crypto */
WillUpdateDevices = CryptoApiCryptoEvent.WillUpdateDevices,
DevicesUpdated = CryptoApiCryptoEvent.DevicesUpdated,
KeysChanged = CryptoApiCryptoEvent.KeysChanged,
/**
* Fires when data is being migrated from legacy crypto to rust crypto.
@@ -273,10 +281,10 @@ export enum CryptoEvent {
* `total` is the total number of steps. When migration is complete, a final instance of the event is emitted, with
* `progress === total === -1`.
*/
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
LegacyCryptoStoreMigrationProgress = CryptoApiCryptoEvent.LegacyCryptoStoreMigrationProgress,
}
export type CryptoEventHandlerMap = {
export type CryptoEventHandlerMap = CryptoApiCryptoEventHandlerMap & {
/**
* Fires when a device is marked as verified/unverified/blocked/unblocked by
* {@link MatrixClient#setDeviceVerified | MatrixClient.setDeviceVerified} or
@@ -287,18 +295,6 @@ export type CryptoEventHandlerMap = {
* @param deviceInfo - updated device information
*/
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, deviceInfo: DeviceInfo) => void;
/**
* Fires when the trust status of a user changes
* If userId is the userId of the logged-in user, this indicated a change
* in the trust status of the cross-signing data on the account.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
* @experimental
*
* @param userId - the userId of the user in question
* @param trustLevel - The new trust level of the user
*/
[CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
/**
* Fires when we receive a room key request
*
@@ -309,28 +305,6 @@ export type CryptoEventHandlerMap = {
* Fires when we receive a room key request cancellation
*/
[CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
/**
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
* @param enabled - true if key backup has been enabled, otherwise false
* @example
* ```
* matrixClient.on("crypto.keyBackupStatus", function(enabled){
* if (enabled) {
* [...]
* }
* });
* ```
*/
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
[CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
/**
* Fires when the backup decryption key is received and cached.
*
* @param version - The version of the backup for which we have the key for.
*/
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
[CryptoEvent.KeySignatureUploadFailure]: (
failures: IUploadKeySignaturesResponse["failures"],
source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
@@ -342,12 +316,6 @@ export type CryptoEventHandlerMap = {
* Deprecated: use `CryptoEvent.VerificationRequestReceived`.
*/
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
/**
* Fires when a key verification request is received.
*/
[CryptoEvent.VerificationRequestReceived]: (request: CryptoApiVerificationRequest) => void;
/**
* Fires when the app may wish to warn the user about something related
* the end-to-end crypto.
@@ -355,33 +323,6 @@ export type CryptoEventHandlerMap = {
* @param type - One of the strings listed above
*/
[CryptoEvent.Warning]: (type: string) => void;
/**
* Fires when the user's cross-signing keys have changed or cross-signing
* has been enabled/disabled. The client can use getStoredCrossSigningForUser
* with the user ID of the logged in user to check if cross-signing is
* enabled on the account. If enabled, it can test whether the current key
* is trusted using with checkUserTrust with the user ID of the logged
* in user. The checkOwnCrossSigningTrust function may be used to reconcile
* the trust in the account key.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
* @experimental
*/
[CryptoEvent.KeysChanged]: (data: {}) => void;
/**
* Fires whenever the stored devices for a user will be updated
* @param users - A list of user IDs that will be updated
* @param initialFetch - If true, the store is empty (apart
* from our own device) and is being seeded.
*/
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
/**
* Fires whenever the stored devices for a user have changed
* @param users - A list of user IDs that were updated
* @param initialFetch - If true, the store was empty (apart
* from our own device) and has been seeded.
*/
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
@@ -1556,7 +1497,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
devices[deviceId].keys[signame],
);
deviceIds.push(deviceId);
} catch (e) {}
} catch {}
}
}
}
@@ -3542,59 +3483,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* resolves once the message has been encrypted and sent to the given
* userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
* of the successfully sent messages.
*
* @deprecated Instead use {@link encryptToDeviceMessages} followed by {@link MatrixClient.queueToDevice}.
*/
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
const toDeviceBatch: ToDeviceBatch = {
eventType: EventType.RoomMessageEncrypted,
batch: [],
};
try {
await Promise.all(
userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
toDeviceBatch.batch.push({
userId,
deviceId,
payload: encryptedContent,
});
await olmlib.ensureOlmSessionsForDevices(
this.olmDevice,
this.baseApis,
new Map([[userId, [deviceInfo]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId,
this.deviceId,
this.olmDevice,
userId,
deviceInfo,
payload,
);
}),
);
// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
if (Object.keys(msg.payload.ciphertext).length > 0) {
return true;
} else {
logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
return false;
}
});
const toDeviceBatch = await this.prepareToDeviceBatch(userDeviceInfoArr, payload);
try {
await this.baseApis.queueToDevice(toDeviceBatch);
@@ -3608,6 +3502,95 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}
private async prepareToDeviceBatch(
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
payload: object,
): Promise<ToDeviceBatch> {
const toDeviceBatch: ToDeviceBatch = {
eventType: EventType.RoomMessageEncrypted,
batch: [],
};
await Promise.all(
userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
toDeviceBatch.batch.push({
userId,
deviceId,
payload: encryptedContent,
});
await olmlib.ensureOlmSessionsForDevices(
this.olmDevice,
this.baseApis,
new Map([[userId, [deviceInfo]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId,
this.deviceId,
this.olmDevice,
userId,
deviceInfo,
payload,
);
}),
);
// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
if (Object.keys(msg.payload.ciphertext).length > 0) {
return true;
} else {
logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
return false;
}
});
return toDeviceBatch;
}
/**
* Implementation of {@link Crypto.CryptoApi#encryptToDeviceMessages}.
*/
public async encryptToDeviceMessages(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch> {
const userIds = new Set(devices.map(({ userId }) => userId));
const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);
const userDeviceInfoArr: IOlmDevice<DeviceInfo>[] = [];
devices.forEach(({ userId, deviceId }) => {
const devices = deviceInfoMap.get(userId);
if (!devices) {
logger.warn(`No devices found for user ${userId}`);
return;
}
if (devices.has(deviceId)) {
// Send the message to a specific device
userDeviceInfoArr.push({ userId, deviceInfo: devices.get(deviceId)! });
} else {
logger.warn(`No device found for user ${userId} with id ${deviceId}`);
}
});
return this.prepareToDeviceBatch(userDeviceInfoArr, payload);
}
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => {
try {
this.onRoomMembership(event, member, oldMembership);
@@ -1216,7 +1216,7 @@ function abortWithException(txn: IDBTransaction, e: Error): void {
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
try {
txn.abort();
} catch (e) {
} catch {
// sometimes we won't be able to abort the transaction
// (ie. if it's aborted or completed)
}
+1 -1
View File
@@ -356,7 +356,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
const sanitizedQsUrlPiece = sanitizedQsString ? `?${sanitizedQsString}` : "";
return asUrl.origin + asUrl.pathname + sanitizedQsUrlPiece;
} catch (error) {
} catch {
// defensive coding for malformed url
return "??";
}
+1 -1
View File
@@ -415,7 +415,7 @@ export class InteractiveAuth<T> {
while (this.submitPromise) {
try {
await this.submitPromise;
} catch (e) {}
} catch {}
}
// use the sessionid from the last request, if one is present.
+12 -13
View File
@@ -57,8 +57,8 @@ export * from "./crypto/store/localStorage-crypto-store.ts";
export * from "./crypto/store/indexeddb-crypto-store.ts";
export type { OutgoingRoomKeyRequest } from "./crypto/store/base.ts";
export * from "./content-repo.ts";
export * from "./@types/common.ts";
export * from "./@types/uia.ts";
export type * from "./@types/common.ts";
export type * from "./@types/uia.ts";
export * from "./@types/event.ts";
export * from "./@types/PushRules.ts";
export * from "./@types/partials.ts";
@@ -70,12 +70,12 @@ export * from "./@types/location.ts";
export * from "./@types/threepids.ts";
export * from "./@types/auth.ts";
export * from "./@types/polls.ts";
export * from "./@types/local_notifications.ts";
export * from "./@types/registration.ts";
export type * from "./@types/local_notifications.ts";
export type * from "./@types/registration.ts";
export * from "./@types/read_receipts.ts";
export * from "./@types/crypto.ts";
export type * from "./@types/crypto.ts";
export * from "./@types/extensible_events.ts";
export * from "./@types/IIdentityServerProvider.ts";
export type * from "./@types/IIdentityServerProvider.ts";
export * from "./@types/membership.ts";
export * from "./models/room-summary.ts";
export * from "./models/event-status.ts";
@@ -93,7 +93,11 @@ export {
GroupCallType,
GroupCallStatsReportEvent,
} from "./webrtc/groupCall.ts";
export { CryptoEvent } from "./crypto/index.ts";
export {
/** @deprecated Use {@link Crypto.CryptoEvent} instead */
CryptoEvent,
} from "./crypto/index.ts";
export { SyncState, SetPresence } from "./sync.ts";
export type { ISyncStateData as SyncStateData } from "./sync.ts";
export { SlidingSyncEvent } from "./sliding-sync.ts";
@@ -108,12 +112,7 @@ export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth.ts";
export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces.ts";
export { LocationAssetType } from "./@types/location.ts";
/**
* Types supporting cryptography.
*
* The most important is {@link Crypto.CryptoApi}, an instance of which can be retrieved via
* {@link MatrixClient.getCrypto}.
*/
/** @deprecated Backwards-compatibility re-export. Import from `crypto-api` directly. */
export * as Crypto from "./crypto-api/index.ts";
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
+101 -45
View File
@@ -38,6 +38,7 @@ import { MatrixError } from "../http-api/errors.ts";
import { MatrixEvent } from "../models/event.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts";
import { sleep } from "../utils.ts";
const logger = rootLogger.getChild("MatrixRTCSession");
@@ -140,6 +141,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
private lastEncryptionKeyUpdateRequest?: number;
private disconnectDelayId: string | undefined;
// We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys
// if it looks like a membership has been updated.
private lastMembershipFingerprints: Set<string> | undefined;
@@ -325,7 +328,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`);
if (joinConfig?.manageMediaKeys) {
this.makeNewSenderKey();
this.requestKeyEventSend();
this.requestSendCurrentKey();
}
// We don't wait for this, mostly because it may fail and schedule a retry, so this
// function returning doesn't really mean anything at all.
@@ -341,11 +344,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
* The membership update required to leave the session will retry if it fails.
* Without network connection the promise will never resolve.
* A timeout can be provided so that there is a guarantee for the promise to resolve.
* @returns Whether the membership update was attempted and did not time out.
*/
public async leaveRoomSession(timeout: number | undefined = undefined): Promise<boolean> {
if (!this.isJoined()) {
logger.info(`Not joined to session in room ${this.room.roomId}: ignoring leave call`);
return new Promise((resolve) => resolve(false));
return false;
}
const userId = this.client.getUserId();
@@ -375,19 +379,15 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.membershipId = undefined;
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
const timeoutPromise = new Promise((r) => {
if (timeout) {
// will never resolve if timeout is not set
setTimeout(r, timeout, "timeout");
}
});
return new Promise((resolve) => {
Promise.race([this.triggerCallMembershipEventUpdate(), timeoutPromise]).then((value) => {
// The timeoutPromise returns the string 'timeout' and the membership update void
// A success implies that the membership update was quicker then the timeout.
resolve(value != "timeout");
});
});
if (timeout) {
// The sleep promise returns the string 'timeout' and the membership update void
// A success implies that the membership update was quicker then the timeout.
const raceResult = await Promise.race([this.triggerCallMembershipEventUpdate(), sleep(timeout, "timeout")]);
return raceResult !== "timeout";
} else {
await this.triggerCallMembershipEventUpdate();
return true;
}
}
public getActiveFocus(): Focus | undefined {
@@ -448,13 +448,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
private getNewEncryptionKeyIndex(): number {
const userId = this.client.getUserId();
const deviceId = this.client.getDeviceId();
if (this.currentEncryptionKeyIndex === -1) {
return 0;
}
if (!userId) throw new Error("No userId!");
if (!deviceId) throw new Error("No deviceId!");
return (this.getKeysForParticipantInternal(userId, deviceId)?.length ?? 0) % 16;
// maximum key index is 255
return (this.currentEncryptionKeyIndex + 1) % 256;
}
/**
@@ -547,10 +546,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
/**
* Requests that we resend our keys to the room. May send a keys event immediately
* Requests that we resend our current keys to the room. May send a keys event immediately
* or queue for alter if one has already been sent recently.
*/
private requestKeyEventSend(): void {
private requestSendCurrentKey(): void {
if (!this.manageMediaKeys) return;
if (
@@ -796,8 +795,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (anyJoined) {
logger.debug(`New member(s) have joined: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestSendCurrentKey();
} else if (oldFingerprints) {
// does it look like any of the members have updated their memberships?
const newFingerprints = this.lastMembershipFingerprints!;
@@ -809,7 +808,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
if (candidateUpdates) {
logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`);
this.requestKeyEventSend();
this.requestSendCurrentKey();
}
}
}
@@ -912,7 +911,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
let membershipObj;
try {
membershipObj = new CallMembership(myCallMemberEvent!, m);
} catch (e) {
} catch {
return false;
}
@@ -1012,19 +1011,24 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
newContent = this.makeNewMembership(localDeviceId);
}
const stateKey = legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId);
try {
await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey);
logger.info(`Sent updated call member event.`);
// check periodically to see if we need to refresh our member event
if (this.isJoined()) {
if (legacy) {
if (legacy) {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
localUserId,
);
if (this.isJoined()) {
// check periodically to see if we need to refresh our member event
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD,
);
} else {
}
} else if (this.isJoined()) {
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
const prepareDelayedDisconnection = async (): Promise<void> => {
try {
// TODO: If delayed event times out, re-join!
const res = await this.client._unstable_sendDelayedStateEvent(
@@ -1036,32 +1040,84 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
{}, // leave event
stateKey,
);
this.scheduleDelayDisconnection(res.delay_id);
this.disconnectDelayId = res.delay_id;
} catch (e) {
logger.error("Failed to send delayed event:", e);
// TODO: Retry if rate-limited
logger.error("Failed to prepare delayed disconnection event:", e);
}
};
await prepareDelayedDisconnection();
// Send join event _after_ preparing the delayed disconnection event
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
stateKey,
);
// If sending state cancels your own delayed state, prepare another delayed state
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
if (this.disconnectDelayId !== undefined) {
try {
await this.client._unstable_updateDelayedEvent(
this.disconnectDelayId,
UpdateDelayedEventAction.Restart,
);
} catch (e) {
// TODO: Make embedded client include errcode, and retry only if not M_NOT_FOUND (or rate-limited)
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
this.disconnectDelayId = undefined;
await prepareDelayedDisconnection();
}
}
if (this.disconnectDelayId !== undefined) {
this.scheduleDelayDisconnection();
}
} else {
let sentDelayedDisconnect = false;
if (this.disconnectDelayId !== undefined) {
try {
await this.client._unstable_updateDelayedEvent(
this.disconnectDelayId,
UpdateDelayedEventAction.Send,
);
sentDelayedDisconnect = true;
} catch (e) {
// TODO: Retry if rate-limited
logger.error("Failed to send our delayed disconnection event:", e);
}
this.disconnectDelayId = undefined;
}
if (!sentDelayedDisconnect) {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
{},
this.makeMembershipStateKey(localUserId, localDeviceId),
);
}
}
logger.info("Sent updated call member event.");
} catch (e) {
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
await new Promise((resolve) => setTimeout(resolve, resendDelay));
await sleep(resendDelay);
await this.triggerCallMembershipEventUpdate();
}
}
private scheduleDelayDisconnection(delayId: string): void {
this.memberEventTimeout = setTimeout(() => this.delayDisconnection(delayId), 5000);
private scheduleDelayDisconnection(): void {
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000);
}
private async delayDisconnection(delayId: string): Promise<void> {
private readonly delayDisconnection = async (): Promise<void> => {
try {
await this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection(delayId);
await this.client._unstable_updateDelayedEvent(this.disconnectDelayId!, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection();
} catch (e) {
logger.error("Failed to delay our disconnection event", e);
// TODO: Retry if rate-limited
logger.error("Failed to delay our disconnection event:", e);
}
}
};
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent> | undefined): boolean {
if (!callMemberEvents?.size) {
+2 -2
View File
@@ -15,8 +15,8 @@ limitations under the License.
*/
export * from "./CallMembership.ts";
export * from "./focus.ts";
export type * from "./focus.ts";
export * from "./LivekitFocus.ts";
export * from "./MatrixRTCSession.ts";
export * from "./MatrixRTCSessionManager.ts";
export * from "./types.ts";
export type * from "./types.ts";
+3 -18
View File
@@ -30,7 +30,6 @@ import {
simpleRetryOperation,
} from "../utils.ts";
import { MSC3089Branch } from "./MSC3089Branch.ts";
import { isRoomSharedHistory } from "../crypto/algorithms/megolm.ts";
import { ISendEventResponse } from "../@types/requests.ts";
import { FileType } from "../http-api/index.ts";
import { KnownMembership } from "../@types/membership.ts";
@@ -136,28 +135,14 @@ export class MSC3089TreeSpace {
* @param userId - The user ID to invite.
* @param andSubspaces - True (default) to invite the user to all
* directories/subspaces too, recursively.
* @param shareHistoryKeys - True (default) to share encryption keys
* with the invited user. This will allow them to decrypt the events (files)
* in the tree. Keys will not be shared if the room is lacking appropriate
* history visibility (by default, history visibility is "shared" in trees,
* which is an appropriate visibility for these purposes).
* @returns Promise which resolves when complete.
*/
public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> {
public async invite(userId: string, andSubspaces = true): Promise<void> {
const promises: Promise<void>[] = [this.retryInvite(userId)];
if (andSubspaces) {
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces, shareHistoryKeys)));
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces)));
}
return Promise.all(promises).then(() => {
// Note: key sharing is default on because for file trees it is relatively important that the invite
// target can actually decrypt the files. The implied use case is that by inviting a user to the tree
// it means the sender would like the receiver to view/download the files contained within, much like
// sharing a folder in other circles.
if (shareHistoryKeys && isRoomSharedHistory(this.room)) {
// noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
this.client.sendSharedHistoryKeys(this.roomId, [userId]);
}
});
await Promise.all(promises);
}
private retryInvite(userId: string): Promise<void> {
+1 -1
View File
@@ -195,7 +195,7 @@ export class IgnoredInvites {
let regexp: RegExp;
try {
regexp = new RegExp(globToRegexp(glob));
} catch (ex) {
} catch {
// Assume invalid event.
continue;
}
+2 -2
View File
@@ -176,7 +176,7 @@ export class RoomReceipts {
* @returns true if the thread with this ID can be found, and the supplied
* user sent the latest message in it.
*/
private userSentLatestEventInThread(threadId: string, userId: String): boolean {
private userSentLatestEventInThread(threadId: string, userId: string): boolean {
const timeline =
threadId === MAIN_ROOM_TIMELINE
? this.room.getLiveTimeline().getEvents()
@@ -278,7 +278,7 @@ class ReceiptsByUser {
private room: Room;
/** map of userId: UserReceipts */
private data: Map<String, UserReceipts>;
private data: Map<string, UserReceipts>;
public constructor(room: Room) {
this.room = room;
+2 -2
View File
@@ -525,7 +525,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
this.threadsTimelineSets[0] = timelineSets[0];
this.threadsTimelineSets[1] = timelineSets[1];
return timelineSets;
} catch (e) {
} catch {
this.threadTimelineSetsPromise = null;
return null;
}
@@ -619,7 +619,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
let capabilities: Capabilities = {};
try {
capabilities = await this.client.getCapabilities();
} catch (e) {}
} catch {}
let versionCap = capabilities["m.room_versions"];
if (!versionCap) {
versionCap = {
+6 -6
View File
@@ -65,7 +65,7 @@ export class TypedEventEmitter<
SuperclassArguments extends ListenerMap<any> = Arguments,
> extends EventEmitter {
/**
* Alias for {@link TypedEventEmitter#on}.
* Alias for {@link on}.
*/
public addListener<T extends Events | EventEmitterEvents>(
event: T,
@@ -119,12 +119,12 @@ export class TypedEventEmitter<
/**
* Returns a copy of the array of listeners for the event named `event`.
*/
public listeners(event: Events | EventEmitterEvents): Function[] {
public listeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["listeners"]> {
return super.listeners(event);
}
/**
* Alias for {@link TypedEventEmitter#removeListener}
* Alias for {@link removeListener}
*/
public off<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
return super.off(event, listener);
@@ -139,7 +139,7 @@ export class TypedEventEmitter<
* being added, and called, multiple times.
*
* By default, event listeners are invoked in the order they are added. The
* {@link TypedEventEmitter#prependListener} method can be used as an alternative to add the
* {@link prependListener} method can be used as an alternative to add the
* event listener to the beginning of the listeners array.
*
* @param event - The name of the event.
@@ -158,7 +158,7 @@ export class TypedEventEmitter<
* Returns a reference to the `EventEmitter`, so that calls can be chained.
*
* By default, event listeners are invoked in the order they are added.
* The {@link TypedEventEmitter#prependOnceListener} method can be used as an alternative to add the
* The {@link prependOnceListener} method can be used as an alternative to add the
* event listener to the beginning of the listeners array.
*
* @param event - The name of the event.
@@ -240,7 +240,7 @@ export class TypedEventEmitter<
* Returns a copy of the array of listeners for the event named `eventName`,
* including any wrappers (such as those created by `.once()`).
*/
public rawListeners(event: Events | EventEmitterEvents): Function[] {
public rawListeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["rawListeners"]> {
return super.rawListeners(event);
}
}
-276
View File
@@ -1,276 +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 { UnstableValue } from "matrix-events-sdk";
import {
RendezvousChannel,
RendezvousFailureListener,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousIntent,
} from "./index.ts";
import { MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client.ts";
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature.ts";
import { logger } from "../logger.ts";
import { sleep } from "../utils.ts";
import { CrossSigningKey } from "../crypto-api/index.ts";
import { Capabilities, Device, IGetLoginTokenCapability } from "../matrix.ts";
enum PayloadType {
Start = "m.login.start",
Finish = "m.login.finish",
Progress = "m.login.progress",
}
enum Outcome {
Success = "success",
Failure = "failure",
Verified = "verified",
Declined = "declined",
Unsupported = "unsupported",
}
export interface MSC3906RendezvousPayload {
type: PayloadType;
intent?: RendezvousIntent;
outcome?: Outcome;
device_id?: string;
device_key?: string;
verifying_device_id?: string;
verifying_device_key?: string;
master_key?: string;
protocols?: string[];
protocol?: string;
login_token?: string;
homeserver?: string;
}
const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc3906.login_token");
/**
* Implements MSC3906 to allow a user to sign in on a new device using QR code.
* This implementation only supports generating a QR code on a device that is already signed in.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3906Rendezvous {
private newDeviceId?: string;
private newDeviceKey?: string;
private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
private _code?: string;
/**
* @param channel - The secure channel used for communication
* @param client - The Matrix client in used on the device already logged in
* @param onFailure - Callback for when the rendezvous fails
*/
public constructor(
private channel: RendezvousChannel<MSC3906RendezvousPayload>,
private client: MatrixClient,
public onFailure?: RendezvousFailureListener,
) {}
/**
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
*/
public get code(): string | undefined {
return this._code;
}
/**
* Generate the code including doing partial set up of the channel where required.
*/
public async generateCode(): Promise<void> {
if (this._code) {
return;
}
this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent));
}
public async startAfterShowingCode(): Promise<string | undefined> {
const checksum = await this.channel.connect();
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
// in stable and unstable r1 the availability is exposed as a capability
let capabilities: Capabilities = {};
try {
capabilities = await this.client.getCapabilities();
} catch (e) {}
// in r0 of MSC3882 the availability is exposed as a feature flag
const features = await buildFeatureSupportMap(await this.client.getVersions());
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
// determine available protocols
if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
logger.info("Server doesn't support get_login_token");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
return undefined;
}
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
logger.info("Waiting for other device to choose protocol");
const { type, protocol, outcome } = await this.receive();
if (type === PayloadType.Finish) {
// new device decided not to complete
switch (outcome ?? "") {
case "unsupported":
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
break;
default:
await this.cancel(RendezvousFailureReason.Unknown);
}
return undefined;
}
if (type !== PayloadType.Progress) {
await this.cancel(RendezvousFailureReason.Unknown);
return undefined;
}
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) {
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
return undefined;
}
return checksum;
}
private async receive(): Promise<MSC3906RendezvousPayload> {
return (await this.channel.receive()) as MSC3906RendezvousPayload;
}
private async send(payload: MSC3906RendezvousPayload): Promise<void> {
await this.channel.send(payload);
}
public async declineLoginOnExistingDevice(): Promise<void> {
logger.info("User declined sign in");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
}
public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> {
// eslint-disable-next-line camelcase
await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl });
logger.info("Waiting for outcome");
const res = await this.receive();
if (!res) {
return undefined;
}
const { outcome, device_id: deviceId, device_key: deviceKey } = res;
if (outcome !== "success") {
throw new Error("Linking failed");
}
this.newDeviceId = deviceId;
this.newDeviceKey = deviceKey;
return deviceId;
}
private async verifyAndCrossSignDevice(deviceInfo: Device): Promise<void> {
const crypto = this.client.getCrypto()!;
if (!this.newDeviceId) {
throw new Error("No new device ID set");
}
// check that keys received from the server for the new device match those received from the device itself
if (deviceInfo.getFingerprint() !== this.newDeviceKey) {
throw new Error(
`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`,
);
}
const userId = this.client.getSafeUserId();
// mark the device as verified locally + cross sign
logger.info(`Marking device ${this.newDeviceId} as verified`);
await crypto.setDeviceVerified(userId, this.newDeviceId, true);
await crypto.crossSignDevice(this.newDeviceId);
const masterPublicKey = (await crypto.getCrossSigningKeyId(CrossSigningKey.Master)) ?? undefined;
const ourDeviceId = this.client.getDeviceId()!;
const ourDeviceKey = (await crypto.getOwnDeviceKeys()).ed25519;
await this.send({
type: PayloadType.Finish,
outcome: Outcome.Verified,
verifying_device_id: ourDeviceId,
verifying_device_key: ourDeviceKey,
master_key: masterPublicKey,
});
}
/**
* Verify the device and cross-sign it.
* @param timeout - time in milliseconds to wait for device to come online
*/
public async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise<void> {
if (!this.newDeviceId) {
throw new Error("No new device to sign");
}
if (!this.newDeviceKey) {
logger.info("No new device key to sign");
return undefined;
}
const crypto = this.client.getCrypto();
if (!crypto) {
throw new Error("Crypto not available on client");
}
let deviceInfo = await this.getOwnDevice(this.newDeviceId);
if (!deviceInfo) {
logger.info("Going to wait for new device to be online");
await sleep(timeout);
deviceInfo = await this.getOwnDevice(this.newDeviceId);
}
if (deviceInfo) {
await this.verifyAndCrossSignDevice(deviceInfo);
return;
}
throw new Error("Device not online within timeout");
}
private async getOwnDevice(deviceId: string): Promise<Device | undefined> {
const userId = this.client.getSafeUserId();
const ownDeviceInfo = await this.client.getCrypto()!.getUserDeviceInfo([userId]);
return ownDeviceInfo.get(userId)?.get(deviceId);
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
this.onFailure?.(reason);
await this.channel.cancel(reason);
}
public async close(): Promise<void> {
await this.channel.close();
}
}
+1 -14
View File
@@ -16,20 +16,7 @@ limitations under the License.
export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void;
export type RendezvousFailureReason =
| LegacyRendezvousFailureReason
| MSC4108FailureReason
| ClientRendezvousFailureReason;
export enum LegacyRendezvousFailureReason {
UserDeclined = "user_declined",
Unknown = "unknown",
Expired = "expired",
UserCancelled = "user_cancelled",
UnsupportedAlgorithm = "unsupported_algorithm",
UnsupportedProtocol = "unsupported_protocol",
HomeserverLacksSupport = "homeserver_lacks_support",
}
export type RendezvousFailureReason = MSC4108FailureReason | ClientRendezvousFailureReason;
export enum MSC4108FailureReason {
AuthorizationExpired = "authorization_expired",
@@ -1,261 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { SAS } from "@matrix-org/olm";
import {
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousChannel,
RendezvousCode,
RendezvousError,
RendezvousIntent,
RendezvousTransport,
RendezvousTransportDetails,
} from "../index.ts";
import { decodeBase64, encodeUnpaddedBase64 } from "../../base64.ts";
import { generateDecimalSas } from "../../crypto/verification/SASDecimal.ts";
import { UnstableValue } from "../../NamespacedValue.ts";
const ECDH_V2 = new UnstableValue(
"m.rendezvous.v2.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
);
export interface ECDHv2RendezvousCode extends RendezvousCode {
rendezvous: {
transport: RendezvousTransportDetails;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key: string;
};
}
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
export interface PlainTextPayload {
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key?: string;
}
export interface EncryptedPayload {
iv: string;
ciphertext: string;
}
async function importKey(key: Uint8Array): Promise<CryptoKey> {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const imported = globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
return imported;
}
/**
* Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
* X25519/ECDH key agreement based secure rendezvous channel.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
private olmSAS?: SAS;
private ourPublicKey: Uint8Array;
private aesKey?: CryptoKey;
private connected = false;
public constructor(
private transport: RendezvousTransport<MSC3903ECDHPayload>,
private theirPublicKey?: Uint8Array,
public onFailure?: (reason: RendezvousFailureReason) => void,
) {
this.olmSAS = new global.Olm.SAS();
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
}
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
if (this.transport.ready) {
throw new Error("Code already generated");
}
await this.transport.send({ algorithm: ECDH_V2.name });
const rendezvous: ECDHv2RendezvousCode = {
rendezvous: {
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
transport: await this.transport.details(),
},
intent,
};
return rendezvous;
}
public async connect(): Promise<string> {
if (this.connected) {
throw new Error("Channel already connected");
}
if (!this.olmSAS) {
throw new Error("Channel closed");
}
const isInitiator = !this.theirPublicKey;
if (isInitiator) {
// wait for the other side to send us their public key
const rawRes = await this.transport.receive();
if (!rawRes) {
throw new Error("No response from other device");
}
const res = rawRes as Partial<PlainTextPayload>;
const { key, algorithm } = res;
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
throw new RendezvousError(
"Unsupported algorithm: " + algorithm,
RendezvousFailureReason.UnsupportedAlgorithm,
);
}
this.theirPublicKey = decodeBase64(key);
} else {
// send our public key unencrypted
await this.transport.send({
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
});
}
this.connected = true;
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
let aesInfo = ECDH_V2.name;
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
this.aesKey = await importKey(aesKeyBytes);
// blank the bytes out to make sure not kept in memory
aesKeyBytes.fill(0);
const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5);
return generateDecimalSas(Array.from(rawChecksum)).join("-");
}
private async encrypt(data: T): Promise<MSC3903ECDHPayload> {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const iv = new Uint8Array(32);
globalThis.crypto.getRandomValues(iv);
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
tagLength: 128,
},
this.aesKey as CryptoKey,
encodedData,
);
return {
iv: encodeUnpaddedBase64(iv),
ciphertext: encodeUnpaddedBase64(ciphertext),
};
}
public async send(payload: T): Promise<void> {
if (!this.olmSAS) {
throw new Error("Channel closed");
}
if (!this.aesKey) {
throw new Error("Shared secret not set up");
}
return this.transport.send(await this.encrypt(payload));
}
private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise<Partial<T>> {
if (!ciphertext || !iv) {
throw new Error("Missing ciphertext and/or iv");
}
const ciphertextBytes = decodeBase64(ciphertext);
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: decodeBase64(iv),
tagLength: 128,
},
this.aesKey as CryptoKey,
ciphertextBytes,
);
return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext)));
}
public async receive(): Promise<Partial<T> | undefined> {
if (!this.olmSAS) {
throw new Error("Channel closed");
}
if (!this.aesKey) {
throw new Error("Shared secret not set up");
}
const rawData = await this.transport.receive();
if (!rawData) {
return undefined;
}
const data = rawData as Partial<EncryptedPayload>;
if (data.ciphertext && data.iv) {
return this.decrypt(data as EncryptedPayload);
}
throw new Error("Data received but no ciphertext");
}
public async close(): Promise<void> {
if (this.olmSAS) {
this.olmSAS.free();
this.olmSAS = undefined;
}
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
try {
await this.transport.cancel(reason);
} finally {
await this.close();
}
}
}
-4
View File
@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3903ECDHv2RendezvousChannel.ts";
export * from "./MSC4108SecureChannel.ts";
+3 -7
View File
@@ -14,16 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3906Rendezvous.ts";
export * from "./MSC4108SignInWithQR.ts";
export * from "./RendezvousChannel.ts";
export * from "./RendezvousCode.ts";
export type * from "./RendezvousChannel.ts";
export type * from "./RendezvousCode.ts";
export * from "./RendezvousError.ts";
export * from "./RendezvousFailureReason.ts";
export * from "./RendezvousIntent.ts";
export * from "./RendezvousTransport.ts";
export type * from "./RendezvousTransport.ts";
export * from "./transports/index.ts";
export * from "./channels/index.ts";
@@ -1,196 +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 { UnstableValue } from "matrix-events-sdk";
import { logger } from "../../logger.ts";
import { sleep } from "../../utils.ts";
import {
RendezvousFailureListener,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousTransport,
RendezvousTransportDetails,
} from "../index.ts";
import { MatrixClient } from "../../matrix.ts";
import { ClientPrefix } from "../../http-api/index.ts";
const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1");
export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails {
uri: string;
}
/**
* Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
* simple HTTP rendezvous protocol.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3886SimpleHttpRendezvousTransport<T extends {}> implements RendezvousTransport<T> {
private uri?: string;
private etag?: string;
private expiresAt?: Date;
private client: MatrixClient;
private fallbackRzServer?: string;
private fetchFn?: typeof global.fetch;
private cancelled = false;
private _ready = false;
public onFailure?: RendezvousFailureListener;
public constructor({
onFailure,
client,
fallbackRzServer,
fetchFn,
}: {
fetchFn?: typeof global.fetch;
onFailure?: RendezvousFailureListener;
client: MatrixClient;
fallbackRzServer?: string;
}) {
this.fetchFn = fetchFn;
this.onFailure = onFailure;
this.client = client;
this.fallbackRzServer = fallbackRzServer;
}
public get ready(): boolean {
return this._ready;
}
public async details(): Promise<MSC3886SimpleHttpRendezvousTransportDetails> {
if (!this.uri) {
throw new Error("Rendezvous not set up");
}
return {
type: TYPE.name,
uri: this.uri,
};
}
private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.fetchFn) {
return this.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
private async getPostEndpoint(): Promise<string | undefined> {
try {
if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) {
return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`;
}
} catch (err) {
logger.warn("Failed to get unstable features", err);
}
return this.fallbackRzServer;
}
public async send(data: T): Promise<void> {
if (this.cancelled) {
return;
}
const method = this.uri ? "PUT" : "POST";
const uri = this.uri ?? (await this.getPostEndpoint());
if (!uri) {
throw new Error("Invalid rendezvous URI");
}
const headers: Record<string, string> = { "content-type": "application/json" };
if (this.etag) {
headers["if-match"] = this.etag;
}
const res = await this.fetch(uri, { method, headers, body: JSON.stringify(data) });
if (res.status === 404) {
return this.cancel(RendezvousFailureReason.Unknown);
}
this.etag = res.headers.get("etag") ?? undefined;
if (method === "POST") {
const location = res.headers.get("location");
if (!location) {
throw new Error("No rendezvous URI given");
}
const expires = res.headers.get("expires");
if (expires) {
this.expiresAt = new Date(expires);
}
// we would usually expect the final `url` to be set by a proper fetch implementation.
// however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback
const baseUrl = res.url ?? uri;
// resolve location header which could be relative or absolute
this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href;
this._ready = true;
}
}
public async receive(): Promise<Partial<T> | undefined> {
if (!this.uri) {
throw new Error("Rendezvous not set up");
}
// eslint-disable-next-line no-constant-condition
while (true) {
if (this.cancelled) {
return undefined;
}
const headers: Record<string, string> = {};
if (this.etag) {
headers["if-none-match"] = this.etag;
}
const poll = await this.fetch(this.uri, { method: "GET", headers });
if (poll.status === 404) {
this.cancel(RendezvousFailureReason.Unknown);
return undefined;
}
// rely on server expiring the channel rather than checking ourselves
if (poll.headers.get("content-type") !== "application/json") {
this.etag = poll.headers.get("etag") ?? undefined;
} else if (poll.status === 200) {
this.etag = poll.headers.get("etag") ?? undefined;
return poll.json();
}
await sleep(1000);
}
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) {
reason = RendezvousFailureReason.Expired;
}
this.cancelled = true;
this._ready = false;
this.onFailure?.(reason);
if (this.uri && reason === RendezvousFailureReason.UserDeclined) {
try {
await this.fetch(this.uri, { method: "DELETE" });
} catch (e) {
logger.warn(e);
}
}
}
}
-4
View File
@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3886SimpleHttpRendezvousTransport.ts";
export * from "./MSC4108RendezvousSession.ts";
@@ -18,10 +18,10 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { OlmMachine } from "@matrix-org/matrix-sdk-crypto-wasm";
import { Curve25519AuthData, KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup.ts";
import { CryptoEvent } from "../crypto-api/index.ts";
import { Logger } from "../logger.ts";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
import { RustBackupManager } from "./backup.ts";
import { CryptoEvent } from "../matrix.ts";
import { encodeUri, sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
@@ -241,7 +241,7 @@ export class PerSessionKeyBackupDownloader {
private async getBackupDecryptionKey(): Promise<RustSdkCryptoJs.BackupKeys | null> {
try {
return await this.olmMachine.getBackupKeys();
} catch (e) {
} catch {
return null;
}
}
+4 -4
View File
@@ -27,13 +27,13 @@ import {
} from "../crypto-api/keybackup.ts";
import { logger } from "../logger.ts";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
import { CryptoEvent, IMegolmSessionData } from "../crypto/index.ts";
import { IMegolmSessionData } from "../crypto/index.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { encodeUri, logDuration } from "../utils.ts";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
import { sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts";
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
import { IKeyBackup } from "../crypto/backup.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
@@ -225,7 +225,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* @returns a promise which resolves once the keys have been imported
*/
public async importRoomKeysAsJson(jsonKeys: string, opts?: ImportRoomKeysOpts): Promise<void> {
await this.olmMachine.importExportedRoomKeys(jsonKeys, (progress: BigInt, total: BigInt): void => {
await this.olmMachine.importExportedRoomKeys(jsonKeys, (progress: bigint, total: bigint): void => {
const importOpt: ImportRoomKeyProgressData = {
total: Number(total),
successes: Number(progress),
@@ -254,7 +254,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
}
await this.olmMachine.importBackedUpRoomKeys(
keysByRoom,
(progress: BigInt, total: BigInt, failures: BigInt): void => {
(progress: bigint, total: bigint, failures: bigint): void => {
const importOpt: ImportRoomKeyProgressData = {
total: Number(total),
successes: Number(progress),
+57 -55
View File
@@ -21,6 +21,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt
import { KnownMembership } from "../@types/membership.ts";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator.ts";
import type { IEncryptedEventInfo } from "../crypto/api.ts";
import type { ToDevicePayload, ToDeviceBatch } from "../models/ToDeviceMessage.ts";
import { MatrixEvent, MatrixEventEvent } from "../models/event.ts";
import { Room } from "../models/room.ts";
import { RoomMember } from "../models/room-member.ts";
@@ -30,7 +31,7 @@ import {
DecryptionError,
OnSyncCompletedData,
} from "../common-crypto/CryptoBackend.ts";
import { logger, Logger } from "../logger.ts";
import { logger, Logger, LogSpan } from "../logger.ts";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api/index.ts";
import { RoomEncryptor } from "./RoomEncryptor.ts";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
@@ -63,6 +64,8 @@ import {
DeviceIsolationMode,
AllDevicesIsolationMode,
DeviceIsolationModeKind,
CryptoEvent,
CryptoEventHandlerMap,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
@@ -72,9 +75,8 @@ import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
import { EventType, MsgType } from "../@types/event.ts";
import { CryptoEvent } from "../crypto/index.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup.ts";
import { RustBackupManager } from "./backup.ts";
import { TypedReEmitter } from "../ReEmitter.ts";
import { randomString } from "../randomstring.ts";
import { ClientStoppedError } from "../errors.ts";
@@ -102,7 +104,7 @@ interface ISignableObject {
*
* @internal
*/
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventHandlerMap> implements CryptoBackend {
/**
* The number of iterations to use when deriving a recovery key from a passphrase.
*/
@@ -125,7 +127,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private outgoingRequestsManager: OutgoingRequestsManager;
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader;
private readonly dehydratedDeviceManager: DehydratedDeviceManager;
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this);
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, CryptoEventHandlerMap>(this);
public constructor(
private readonly logger: Logger,
@@ -1315,6 +1317,52 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return secrets;
}
/**
* Implementation of {@link CryptoApi#encryptToDeviceMessages}.
*/
public async encryptToDeviceMessages(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<ToDeviceBatch> {
const logger = new LogSpan(this.logger, "encryptToDeviceMessages");
const uniqueUsers = new Set(devices.map(({ userId }) => userId));
// This will ensure we have Olm sessions for all of the users' devices.
// However, we only care about some of the devices.
// So, perhaps we can optimise this later on.
await this.keyClaimManager.ensureSessionsForUsers(
logger,
Array.from(uniqueUsers).map((userId) => new RustSdkCryptoJs.UserId(userId)),
);
const batch: ToDeviceBatch = {
batch: [],
eventType: EventType.RoomMessageEncrypted,
};
await Promise.all(
devices.map(async ({ userId, deviceId }) => {
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
new RustSdkCryptoJs.UserId(userId),
new RustSdkCryptoJs.DeviceId(deviceId),
);
if (device) {
const encryptedPayload = JSON.parse(await device.encryptToDeviceEvent(eventType, payload));
batch.batch.push({
deviceId,
userId,
payload: encryptedPayload,
});
} else {
this.logger.warn(`encryptToDeviceMessages: unknown device ${userId}:${deviceId}`);
}
}),
);
return batch;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
@@ -1536,7 +1584,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private onRoomKeyUpdated(key: RustSdkCryptoJs.RoomKeyInfo): void {
if (this.stopped) return;
this.logger.debug(
`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`,
`Got update for session ${key.sessionId} from sender ${key.senderKey.toBase64()} in ${key.roomId.toString()}`,
);
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key.roomId.toString(), key.sessionId);
if (pendingList.length === 0) return;
@@ -1841,7 +1889,7 @@ class EventDecryptor {
serverBackupInfo: KeyBackupInfo | null | undefined,
): never {
const content = event.getWireContent();
const errorDetails = { session: content.sender_key + "|" + content.session_id };
const errorDetails = { sender_key: content.sender_key, session_id: content.session_id };
// If the error looks like it might be recoverable from backup, queue up a request to try that.
if (
@@ -2077,51 +2125,5 @@ function rustEncryptionInfoToJsEncryptionInfo(
return { shieldColour, shieldReason };
}
type RustCryptoEvents =
| CryptoEvent.VerificationRequestReceived
| CryptoEvent.UserTrustStatusChanged
| CryptoEvent.KeysChanged
| CryptoEvent.WillUpdateDevices
| CryptoEvent.DevicesUpdated
| RustBackupCryptoEvents;
type RustCryptoEventMap = {
/**
* Fires when a key verification request is received.
*/
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
/**
* Fires when the trust status of a user changes.
*/
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void;
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
/**
* Fires when the user's cross-signing keys have changed or cross-signing
* has been enabled/disabled. The client can use getStoredCrossSigningForUser
* with the user ID of the logged in user to check if cross-signing is
* enabled on the account. If enabled, it can test whether the current key
* is trusted using with checkUserTrust with the user ID of the logged
* in user. The checkOwnCrossSigningTrust function may be used to reconcile
* the trust in the account key.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
* @experimental
*/
[CryptoEvent.KeysChanged]: (data: {}) => void;
/**
* Fires whenever the stored devices for a user will be updated
* @param users - A list of user IDs that will be updated
* @param initialFetch - If true, the store is empty (apart
* from our own device) and is being seeded.
*/
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
/**
* Fires whenever the stored devices for a user have changed
* @param users - A list of user IDs that were updated
* @param initialFetch - If true, the store was empty (apart
* from our own device) and has been seeded.
*/
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
} & RustBackupCryptoEventMap;
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
type RustCryptoEvents = Exclude<CryptoEvents, CryptoEvent.LegacyCryptoStoreMigrationProgress>;
+17 -2
View File
@@ -28,7 +28,6 @@ import { logger } from "./logger.ts";
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "./crypto/aes.ts";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -165,7 +164,7 @@ export interface SecretStorageCallbacks {
* Descriptions of the secret storage keys are also stored in server-side storage, per the
* [matrix specification](https://spec.matrix.org/v1.6/client-server-api/#key-storage), so
* before a key can be used in this way, it must have been stored on the server. This is
* done via {@link SecretStorage.ServerSideSecretStorage#addKey}.
* done via {@link ServerSideSecretStorage#addKey}.
*
* Obviously the keys themselves are not stored server-side, so the js-sdk calls this callback
* in order to retrieve a secret storage key from the application.
@@ -676,3 +675,19 @@ export function trimTrailingEquals(input: string): string {
return input;
}
}
// string of zeroes, for calculating the key check
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Calculate the MAC for checking the key.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
}
+13 -10
View File
@@ -24,7 +24,7 @@ import { logger } from "../logger.ts";
import { ISavedSync } from "./index.ts";
import { IIndexedDBBackend } from "./indexeddb-backend.ts";
import { ISyncResponse } from "../sync-accumulator.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { IStateEventWithRoomId } from "../@types/search.ts";
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage.ts";
import { IStoredClientOpts } from "../client.ts";
@@ -118,7 +118,10 @@ export class IndexedDBStore extends MemoryStore {
}
}
public on = this.emitter.on.bind(this.emitter);
/** Re-exports `TypedEventEmitter.on` */
public on(event: EventEmitterEvents | "degraded" | "closed", handler: (...args: any[]) => void): void {
this.emitter.on(event, handler);
}
/**
* @returns Resolved when loaded from indexed db.
@@ -203,7 +206,7 @@ export class IndexedDBStore extends MemoryStore {
throw err;
},
);
});
}, null);
/**
* Whether this store would like to save its data
@@ -250,7 +253,7 @@ export class IndexedDBStore extends MemoryStore {
}
return this.backend.syncToDatabase(userTuples);
});
}, null);
public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
return this.backend.setSyncData(syncData);
@@ -307,13 +310,13 @@ export class IndexedDBStore extends MemoryStore {
* @param fallback - The method name for fallback.
* @returns A wrapped member function.
*/
private degradable<A extends Array<any>, R = void>(
private degradable<A extends Array<any>, F extends keyof MemoryStore | null, R = void>(
func: DegradableFn<A, R>,
fallback?: keyof MemoryStore,
): DegradableFn<A, R> {
const fallbackFn = fallback ? (super[fallback] as Function) : null;
fallback: F,
): DegradableFn<A, F extends string ? R : void> {
const fallbackFn = fallback ? (super[fallback] as (...args: A) => Promise<R>) : null;
return async (...args) => {
return (async (...args) => {
try {
return await func.call(this, ...args);
} catch (e) {
@@ -341,7 +344,7 @@ export class IndexedDBStore extends MemoryStore {
return fallbackFn.call(this, ...args);
}
}
};
}) as DegradableFn<A, F extends string ? R : void>;
}
// XXX: ideally these would be stored in indexeddb as part of the room but,
+2 -2
View File
@@ -258,7 +258,7 @@ export class MemoryStore implements IStore {
if (isValidFilterId(value)) {
return value;
}
} catch (e) {}
} catch {}
return null;
}
@@ -276,7 +276,7 @@ export class MemoryStore implements IStore {
} else {
this.localStorage.removeItem(key);
}
} catch (e) {}
} catch {}
}
/**
+2 -2
View File
@@ -782,7 +782,7 @@ export class GroupCall extends TypedEventEmitter<
return false;
}
}
} catch (e) {
} catch {
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
@@ -817,7 +817,7 @@ export class GroupCall extends TypedEventEmitter<
await this.updateLocalUsermediaStream(stream);
this.localCallFeed.setAudioVideoMuted(null, muted);
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
} catch (_) {
} catch {
// No permission to video device
/* istanbul ignore next */
logger.log(
+1 -1
View File
@@ -17,7 +17,7 @@ export interface CallStatsReportSummary {
audioTrackSummary: TrackSummary;
videoTrackSummary: TrackSummary;
isFirstCollection: Boolean;
isFirstCollection: boolean;
}
export interface TrackSummary {
+1 -1
View File
@@ -50,7 +50,7 @@ export class TrackStatsBuilder {
*/
const codecShortType = codec.mimeType.split("/")[1];
codecShortType && trackStats.setCodec(codecShortType);
if (codecShortType) trackStats.setCodec(codecShortType);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
"coverageLabel": "TypeDoc",
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts", "src/utils/*.ts"],
"entryPoints": ["src/matrix.ts", "src/crypto-api", "src/types.ts", "src/testing.ts", "src/utils/*.ts"],
"excludeExternals": true,
"out": "_docs"
}
+941 -947
View File
File diff suppressed because it is too large Load Diff