Compare commits
46 Commits
v34.7.0-rc.0
...
v34.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d4ff4c3f2 | |||
| 00aba742e4 | |||
| c4048d985d | |||
| a8f8a9c14d | |||
| 0e2f73d7a7 | |||
| 31aeb3044f | |||
| 0a29063bc9 | |||
| 3cc3bd0728 | |||
| f891fe4423 | |||
| b99ff83785 | |||
| 23c4c9fd8a | |||
| 8b8ee91210 | |||
| d8c431f23e | |||
| a6fb7530cb | |||
| b5c3f15a67 | |||
| 91f6f0f9c5 | |||
| 88cf5eb769 | |||
| 13a967ae8f | |||
| 66c80949e8 | |||
| 3d51e31da8 | |||
| 86c190fa0d | |||
| e9a9280e3c | |||
| fabdd6da64 | |||
| ab5d95102c | |||
| b4556d6552 | |||
| 6c22da9a96 | |||
| d29329bddb | |||
| 68b06d7b60 | |||
| 5f599ee978 | |||
| aabd558bce | |||
| 662b772c73 | |||
| b240c44128 | |||
| 5508993d79 | |||
| c9b43ab251 | |||
| 2fb1e659c8 | |||
| b842192a34 | |||
| 868bbfcb31 | |||
| 860161bdd2 | |||
| 0c9d82e40a | |||
| 73ab7c342a | |||
| 3386c66b98 | |||
| da044820d7 | |||
| 9f40f32b3e | |||
| f679942584 | |||
| 1dcb7a6e75 | |||
| 823316b2ff |
+4
-6
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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?
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("secret-storage", () => {
|
||||
} else if (secretName == "secretE") {
|
||||
return { aaaa: {}, bbbb: {} };
|
||||
} else {
|
||||
null;
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 "??";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -195,7 +195,7 @@ export class IgnoredInvites {
|
||||
let regexp: RegExp;
|
||||
try {
|
||||
regexp = new RegExp(globToRegexp(glob));
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Assume invalid event.
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface CallStatsReportSummary {
|
||||
audioTrackSummary: TrackSummary;
|
||||
videoTrackSummary: TrackSummary;
|
||||
|
||||
isFirstCollection: Boolean;
|
||||
isFirstCollection: boolean;
|
||||
}
|
||||
|
||||
export interface TrackSummary {
|
||||
|
||||
@@ -50,7 +50,7 @@ export class TrackStatsBuilder {
|
||||
*/
|
||||
const codecShortType = codec.mimeType.split("/")[1];
|
||||
|
||||
codecShortType && trackStats.setCodec(codecShortType);
|
||||
if (codecShortType) trackStats.setCodec(codecShortType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user