Compare commits
61 Commits
v32.2.0-rc.0
...
v33.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a4c4e7e275 | |||
| 222e95d33f | |||
| 2ee43cade7 | |||
| 395c3cfcd6 | |||
| f95954c233 | |||
| fa5f2d389a | |||
| 9fc557fc6b | |||
| 6436fbb99f | |||
| 87c2ac3ffa | |||
| 43022d5b2f | |||
| a0fadeb4ec | |||
| a3cea8ce7d | |||
| c88487da07 | |||
| 89875b8e31 | |||
| 9c94393d76 | |||
| 7850294a4b | |||
| 131e81401a | |||
| 5c27e30302 | |||
| 8dfb6de3cc | |||
| 042610310f | |||
| 8535604200 | |||
| 3ee64722c5 | |||
| 9d6210b3f9 | |||
| 909caab74e | |||
| 7c87625157 | |||
| b19817bb73 | |||
| 36196ea422 | |||
| a81adf542e | |||
| a49bc3ddf4 | |||
| a86d4ceb49 | |||
| 8c3be2a56a | |||
| 38898a60c7 | |||
| fd3a4d4403 | |||
| 93d96281fd | |||
| 944dc51c58 | |||
| c6b43dd176 | |||
| f03dd7b7bc | |||
| 51fa1866a9 | |||
| d76fb2baa0 | |||
| 3feafc9c17 | |||
| c9075b3dba | |||
| 69c474dda7 | |||
| 73ce51065f | |||
| 5d0407d0a6 | |||
| 3a4b02d8e6 | |||
| d421e7f829 | |||
| 9fd051af33 | |||
| b78a1ad889 | |||
| a25cdcecaa | |||
| 2a716bd076 | |||
| ef1db8d664 | |||
| 22865fd834 | |||
| c4fe564855 | |||
| 9ecb1a0381 | |||
| ef9490c7b1 | |||
| 402adfbe8a | |||
| 41e8c2af34 | |||
| 4843b40296 | |||
| bc2c870152 | |||
| 7c7b2817d3 | |||
| 9f78202ecd |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
@@ -27,6 +27,7 @@ jobs:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
- repo: element-hq/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@132879b972cb7f2ac593006455875098e73cc7f2 # v5
|
||||
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
|
||||
@@ -71,6 +71,24 @@ jobs:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
|
||||
# we need this so the job is reported properly when run in a merge queue
|
||||
downstream-complement-crypto:
|
||||
name: Downstream Complement Crypto tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
steps:
|
||||
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
# Hook for branch protection to skip downstream testing outside of merge queues
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
|
||||
@@ -6,6 +6,6 @@ on:
|
||||
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,75 @@
|
||||
Changes in [33.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.1.0) (2024-06-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* MSC4108 support OIDC QR code login ([#4134](https://github.com/matrix-org/matrix-js-sdk/pull/4134)). Contributed by @t3chguy.
|
||||
* Add crypto methods for export and import of secrets bundle ([#4227](https://github.com/matrix-org/matrix-js-sdk/pull/4227)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix screen sharing in recent Chrome ([#4243](https://github.com/matrix-org/matrix-js-sdk/pull/4243)). Contributed by @RiotRobot.
|
||||
* Fix incorrect assumptions about required fields in /search response ([#4228](https://github.com/matrix-org/matrix-js-sdk/pull/4228)). Contributed by @t3chguy.
|
||||
* Fix the queueToDevice tests for the new fakeindexeddb ([#4225](https://github.com/matrix-org/matrix-js-sdk/pull/4225)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [33.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.0.0) (2024-06-04)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
|
||||
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
|
||||
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* `initRustCrypto`: allow app to pass in the store key directly ([#4210](https://github.com/matrix-org/matrix-js-sdk/pull/4210)). Contributed by @richvdh.
|
||||
* Preserve ESM for async imports to work correctly ([#4187](https://github.com/matrix-org/matrix-js-sdk/pull/4187)). Contributed by @ms-dosx86.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Don't run migration for Rust crypto if the legacy store is empty ([#4218](https://github.com/matrix-org/matrix-js-sdk/pull/4218)). Contributed by @andybalaam.
|
||||
* Bump matrix-sdk-crypto-wasm to 5.0.0 ([#4216](https://github.com/matrix-org/matrix-js-sdk/pull/4216)). Contributed by @richvdh.
|
||||
* Wire up verification cancel \& mismatch for rust crypto ([#4202](https://github.com/matrix-org/matrix-js-sdk/pull/4202)). Contributed by @t3chguy.
|
||||
* Only pass id\_server if we had one to begin with ([#4200](https://github.com/matrix-org/matrix-js-sdk/pull/4200)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [32.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.4.0) (2024-05-22)
|
||||
==================================================================================================
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [32.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.3.0) (2024-05-21)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Simplify OIDC types \& export `decodeIdToken` ([#4193](https://github.com/matrix-org/matrix-js-sdk/pull/4193)). Contributed by @t3chguy.
|
||||
* Add helpers for authenticated media, and associated documentation ([#4185](https://github.com/matrix-org/matrix-js-sdk/pull/4185)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix state\_events.ts types ([#4196](https://github.com/matrix-org/matrix-js-sdk/pull/4196)). Contributed by @t3chguy.
|
||||
* Fix sendEventHttpRequest for `m.room.redaction` events without `redacts` ([#4192](https://github.com/matrix-org/matrix-js-sdk/pull/4192)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
|
||||
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
|
||||
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
|
||||
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
|
||||
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -21,6 +21,10 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
# Quickstart
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
@@ -89,6 +93,34 @@ Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
});
|
||||
```
|
||||
|
||||
## Authenticated media
|
||||
|
||||
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) will require clients, like
|
||||
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
|
||||
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
|
||||
to append the header when using the endpoints in `<img />` elements and similar.
|
||||
|
||||
```javascript
|
||||
const downloadUrl = client.mxcUrlToHttp(
|
||||
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
|
||||
/*width=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*height=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*allowDirectLinks=*/ false, // should generally be left `false`.
|
||||
/*allowRedirects=*/ true, // implied supported with authentication
|
||||
/*useAuthentication=*/ true, // the flag we're after in this example
|
||||
);
|
||||
const img = await fetch(downloadUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${client.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
// Do something with `img`.
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
|
||||
|
||||
## What does this SDK do?
|
||||
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
};
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "32.2.0-rc.0",
|
||||
"version": "33.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -53,7 +53,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.9.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^5.0.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -105,8 +105,8 @@
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"eslint-plugin-unicorn": "^53.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"prettier": "3.3.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.25.10",
|
||||
|
||||
@@ -630,6 +630,27 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
|
||||
});
|
||||
|
||||
newBackendOnly(
|
||||
"fails with NOT_JOINED if user is not member of room (MSC4115 unstable prefix)",
|
||||
async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "leave",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(
|
||||
DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room",
|
||||
async () => {
|
||||
@@ -654,6 +675,30 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)",
|
||||
async () => {
|
||||
// This tests that when the server reports that the user
|
||||
// was invited at the time the event was sent, then we
|
||||
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
|
||||
// and instead get some other error, since the user should
|
||||
// have gotten the key for the event.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "invite",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room",
|
||||
async () => {
|
||||
@@ -676,6 +721,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)",
|
||||
async () => {
|
||||
// This tests that when the server reports the user's
|
||||
// membership, and reports that the user was joined, then we
|
||||
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
|
||||
// instead get some other error.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const ev = await sendEventAndAwaitDecryption({
|
||||
unsigned: {
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "join",
|
||||
},
|
||||
});
|
||||
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("Decryption fails with Unable to decrypt for other errors", async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
CryptoApi,
|
||||
Crypto,
|
||||
CryptoEvent,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
@@ -310,7 +310,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
describe("recover from backup", () => {
|
||||
let aliceCrypto: CryptoApi;
|
||||
let aliceCrypto: Crypto.CryptoApi;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump"
|
||||
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
|
||||
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
|
||||
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
|
||||
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
|
||||
@@ -85,6 +86,44 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storageKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storagePassword", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -266,6 +305,38 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not migrate if account data is missing", async () => {
|
||||
// See https://github.com/element-hq/element-web/issues/27447
|
||||
|
||||
// Given we have an almost-empty legacy account in the database
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: EMPTY_ACCOUNT_DATASET.userId,
|
||||
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
// When we start Rust crypto, potentially triggering an upgrade
|
||||
const progressListener = jest.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// Then no error occurs, and no upgrade happens
|
||||
expect(progressListener.mock.calls.length).toBe(0);
|
||||
}, 60000);
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
|
||||
@@ -1825,7 +1825,6 @@ function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
|
||||
const buildEventMessageInThread = (root: MatrixEvent) =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1846,12 +1845,10 @@ const buildEventMessageInThread = (root: MatrixEvent) =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80098509 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollResponseReference = () =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1869,7 +1866,6 @@ const buildEventPollResponseReference = () =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80106237 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReaction = (event: MatrixEvent) =>
|
||||
@@ -1909,7 +1905,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
|
||||
|
||||
const buildEventPollStartThreadRoot = () =>
|
||||
new MatrixEvent({
|
||||
age: 80108647,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "ENCRYPTEDSTUFF",
|
||||
@@ -1923,12 +1918,10 @@ const buildEventPollStartThreadRoot = () =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80108647 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReply = (target: MatrixEvent) =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1947,12 +1940,10 @@ const buildEventReply = (target: MatrixEvent) =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80098509 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventRoomName = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123249,
|
||||
content: {
|
||||
name: "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
@@ -1963,12 +1954,10 @@ const buildEventRoomName = () =>
|
||||
state_key: "",
|
||||
type: "m.room.name",
|
||||
unsigned: { age: 80123249 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventEncryption = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123383,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
@@ -1979,12 +1968,10 @@ const buildEventEncryption = () =>
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
unsigned: { age: 80123383 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventGuestAccess = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123473,
|
||||
content: {
|
||||
guest_access: "can_join",
|
||||
},
|
||||
@@ -1995,12 +1982,10 @@ const buildEventGuestAccess = () =>
|
||||
state_key: "",
|
||||
type: "m.room.guest_access",
|
||||
unsigned: { age: 80123473 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventHistoryVisibility = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123556,
|
||||
content: {
|
||||
history_visibility: "shared",
|
||||
},
|
||||
@@ -2011,12 +1996,10 @@ const buildEventHistoryVisibility = () =>
|
||||
state_key: "",
|
||||
type: "m.room.history_visibility",
|
||||
unsigned: { age: 80123556 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventJoinRules = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123696,
|
||||
content: {
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
@@ -2027,12 +2010,10 @@ const buildEventJoinRules = () =>
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
unsigned: { age: 80123696 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPowerLevels = () =>
|
||||
new MatrixEvent({
|
||||
age: 80124105,
|
||||
content: {
|
||||
ban: 50,
|
||||
events: {
|
||||
@@ -2063,12 +2044,10 @@ const buildEventPowerLevels = () =>
|
||||
state_key: "",
|
||||
type: "m.room.power_levels",
|
||||
unsigned: { age: 80124105 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventMember = () =>
|
||||
new MatrixEvent({
|
||||
age: 80125279,
|
||||
content: {
|
||||
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
displayname: "andybalaam-test1",
|
||||
@@ -2081,12 +2060,10 @@ const buildEventMember = () =>
|
||||
state_key: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.member",
|
||||
unsigned: { age: 80125279 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventCreate = () =>
|
||||
new MatrixEvent({
|
||||
age: 80126105,
|
||||
content: {
|
||||
room_version: "6",
|
||||
},
|
||||
@@ -2097,7 +2074,6 @@ const buildEventCreate = () =>
|
||||
state_key: "",
|
||||
type: "m.room.create",
|
||||
unsigned: { age: 80126105 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj: Record<string, any>, expected: any): void {
|
||||
|
||||
@@ -333,7 +333,7 @@ describe("MatrixClient room timelines", function () {
|
||||
name: userName,
|
||||
url: "mxc://some/url",
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
oldMshipEvent.unsigned!.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: undefined,
|
||||
membership: KnownMembership.Join,
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
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 { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import {
|
||||
MSC4108FailureReason,
|
||||
MSC4108RendezvousSession,
|
||||
MSC4108SecureChannel,
|
||||
MSC4108SignInWithQR,
|
||||
PayloadType,
|
||||
RendezvousError,
|
||||
} from "../../../src/rendezvous";
|
||||
import { defer } from "../../../src/utils";
|
||||
import {
|
||||
ClientPrefix,
|
||||
DEVICE_CODE_SCOPE,
|
||||
IHttpOpts,
|
||||
IMyDevice,
|
||||
MatrixClient,
|
||||
MatrixError,
|
||||
MatrixHttpApi,
|
||||
} from "../../../src";
|
||||
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const baseUrl = "https://example.com";
|
||||
const crypto = {
|
||||
exportSecretsForQrLogin: jest.fn(),
|
||||
};
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl,
|
||||
getHomeserverUrl() {
|
||||
return baseUrl;
|
||||
},
|
||||
getDevice: jest.fn(),
|
||||
getCrypto: jest.fn(() => crypto),
|
||||
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
prefix: ClientPrefix.Unstable,
|
||||
onlyData: true,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("MSC4108SignInWithQR", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get(
|
||||
"https://issuer/.well-known/openid-configuration",
|
||||
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
|
||||
);
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
const deviceId = "DEADB33F";
|
||||
const verificationUri = "https://example.com/verify";
|
||||
const verificationUriComplete = "https://example.com/verify/complete";
|
||||
|
||||
it("should generate qr code data as expected", async () => {
|
||||
const session = new MSC4108RendezvousSession({
|
||||
url,
|
||||
});
|
||||
const channel = new MSC4108SecureChannel(session);
|
||||
const login = new MSC4108SignInWithQR(channel, false);
|
||||
|
||||
await login.generateCode();
|
||||
const code = login.code;
|
||||
expect(code).toHaveLength(71);
|
||||
const text = new TextDecoder().decode(code);
|
||||
expect(text.startsWith("MATRIX")).toBeTruthy();
|
||||
expect(text.endsWith(url)).toBeTruthy();
|
||||
|
||||
// Assert that the code is stable
|
||||
await login.generateCode();
|
||||
expect(login.code).toEqual(code);
|
||||
});
|
||||
|
||||
describe("should be able to connect as a reciprocating device", () => {
|
||||
let client: MatrixClient;
|
||||
let ourLogin: MSC4108SignInWithQR;
|
||||
let opponentLogin: MSC4108SignInWithQR;
|
||||
|
||||
beforeEach(async () => {
|
||||
let ourData = defer<string>();
|
||||
let opponentData = defer<string>();
|
||||
|
||||
const ourMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
ourData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
const prom = opponentData.promise;
|
||||
prom.then(() => {
|
||||
opponentData = defer();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
cancelled: false,
|
||||
cancel: () => {
|
||||
// @ts-ignore
|
||||
ourMockSession.cancelled = true;
|
||||
ourData.resolve("");
|
||||
},
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const opponentMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
opponentData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
const prom = ourData.promise;
|
||||
prom.then(() => {
|
||||
ourData = defer();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
|
||||
client = makeMockClient({ userId: "@alice:example.com", deviceId: "alice", msc4108Enabled: true });
|
||||
|
||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||
const qrCodeData = QrCodeData.from_bytes(
|
||||
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getHomeserverUrl()),
|
||||
);
|
||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.public_key);
|
||||
|
||||
ourLogin = new MSC4108SignInWithQR(ourChannel, true, client);
|
||||
opponentLogin = new MSC4108SignInWithQR(opponentChannel, false);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share homeserver url & check code", async () => {
|
||||
await Promise.all([
|
||||
expect(ourLogin.negotiateProtocols()).resolves.toEqual({}),
|
||||
expect(opponentLogin.negotiateProtocols()).resolves.toEqual({ homeserverBaseUrl: client.baseUrl }),
|
||||
]);
|
||||
|
||||
expect(ourLogin.checkCode).toBe(opponentLogin.checkCode);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||
verificationUri: verificationUriComplete,
|
||||
}),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
verification_uri_complete: verificationUriComplete,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device already exists", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort on unsupported protocol", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow(
|
||||
"Received a request for an unsupported protocol",
|
||||
),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant_v2",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share secrets", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
|
||||
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
|
||||
const payload = {
|
||||
secrets: expect.objectContaining(secrets),
|
||||
};
|
||||
await Promise.all([
|
||||
expect(ourProm).resolves.toEqual(payload),
|
||||
expect(opponentLogin.shareSecrets()).resolves.toEqual(payload),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
return -1;
|
||||
});
|
||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
await expect(ourProm).rejects.toThrow("New device not found");
|
||||
});
|
||||
|
||||
it("should abort on unexpected errors", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||
);
|
||||
|
||||
await expect(ourLogin.shareSecrets()).rejects.toThrow("The message");
|
||||
});
|
||||
|
||||
it("should abort on declined login", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await ourLogin.declineLoginOnExistingDevice();
|
||||
await expect(opponentLogin.shareSecrets()).rejects.toThrow(
|
||||
new RendezvousError("Failed", MSC4108FailureReason.UserCancelled),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
return -1;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
const opponentProm = opponentLogin.shareSecrets();
|
||||
|
||||
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
const deferred = defer<IMyDevice>();
|
||||
mocked(client.getDevice).mockReturnValue(deferred.promise);
|
||||
|
||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||
deferred.resolve({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||
expect(opponentProm).rejects.toThrow("Unexpected message received"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,10 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns ValidatedIssuerMetadata
|
||||
*/
|
||||
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
|
||||
export const mockOpenIdConfiguration = (
|
||||
issuer = "https://auth.org/",
|
||||
additionalGrantTypes: string[] = [],
|
||||
): ValidatedIssuerMetadata => ({
|
||||
issuer,
|
||||
revocation_endpoint: issuer + "revoke",
|
||||
token_endpoint: issuer + "token",
|
||||
@@ -47,6 +50,6 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
|
||||
@@ -173,8 +173,10 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
unsigned: opts.unsigned || {},
|
||||
unsigned: {
|
||||
...opts.unsigned,
|
||||
prev_content: opts.prev_content,
|
||||
},
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
|
||||
|
||||
A dump of an account which is almost completely empty, and totally unsuitable
|
||||
for use as a real account.
|
||||
|
||||
This dump was manually created by copying and editing full_account.
|
||||
|
||||
Created to test
|
||||
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
|
||||
We should not launch the Rust migration code when we find a DB in this state.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"account": [],
|
||||
"device_data": [],
|
||||
"inbound_group_sessions": [],
|
||||
"inbound_group_sessions_withheld": [],
|
||||
"notified_error_devices": [],
|
||||
"outgoingRoomKeyRequests": [],
|
||||
"parked_shared_history": [],
|
||||
"rooms": [],
|
||||
"session_problems": [],
|
||||
"sessions": [],
|
||||
"sessions_needing_backup": [],
|
||||
"shared_history_inbound_group_sessions": []
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEYS_QUERY_RESPONSE = { device_keys: { "@emptyuser:example.com": {} } };
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const EMPTY_ACCOUNT_DATASET: DumpDataSetInfo = {
|
||||
userId: "@emptyuser:example.com",
|
||||
deviceId: "EMPTYDEVIC",
|
||||
pickleKey: "+/bcdefghijklmnopqrstu1/zyxvutsrqponmlkjih2",
|
||||
keyQueryResponse: KEYS_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/empty_account/dump.json",
|
||||
};
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
|
||||
@@ -857,7 +857,7 @@ describe("AutoDiscovery", function () {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
|
||||
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
|
||||
@@ -76,5 +76,29 @@ describe("ContentRepo", function () {
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an authenticated URL when requested", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, true, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should force-enable allow_redirects when useAuthentication is set true", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,12 +23,12 @@ import HttpBackend from "matrix-mock-request";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { CryptoEvent } from "../../../src/crypto";
|
||||
import { IDevice } from "../../../src/crypto/deviceinfo";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
|
||||
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../../../src/crypto-api";
|
||||
|
||||
const PUSH_RULES_RESPONSE: Response = {
|
||||
method: "GET",
|
||||
@@ -377,7 +377,7 @@ describe("Cross Signing", function () {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -515,7 +515,7 @@ describe("Cross Signing", function () {
|
||||
};
|
||||
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
|
||||
|
||||
const bobMaster: ICrossSigningKey = {
|
||||
const bobMaster: CrossSigningKeyInfo = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
@@ -630,7 +630,7 @@ describe("Cross Signing", function () {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -696,7 +696,7 @@ describe("Cross Signing", function () {
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: ICrossSigningKey = {
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -763,7 +763,7 @@ describe("Cross Signing", function () {
|
||||
const bobSigning2 = new global.Olm.PkSigning();
|
||||
const bobPrivkey2 = bobSigning2.generate_seed();
|
||||
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
|
||||
const bobSSK2: ICrossSigningKey = {
|
||||
const bobSSK2: CrossSigningKeyInfo = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -913,7 +913,7 @@ describe("Cross Signing", function () {
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -988,7 +988,7 @@ describe("Cross Signing", function () {
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
@@ -1048,7 +1048,7 @@ describe("Cross Signing", function () {
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: ICrossSigningKey = {
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
|
||||
@@ -23,12 +23,13 @@ import { makeTestClients } from "./verification/util";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
|
||||
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { ISignatures } from "../../../src/@types/signed";
|
||||
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
|
||||
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
@@ -475,7 +476,7 @@ describe("Secrets", function () {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign<ICrossSigningKey>(
|
||||
self_signing: sign<CrossSigningKeyInfo>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
@@ -486,7 +487,7 @@ describe("Secrets", function () {
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
user_signing: sign<ICrossSigningKey>(
|
||||
user_signing: sign<CrossSigningKeyInfo>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
@@ -631,7 +632,7 @@ describe("Secrets", function () {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign<ICrossSigningKey>(
|
||||
self_signing: sign<CrossSigningKeyInfo>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
@@ -642,7 +643,7 @@ describe("Secrets", function () {
|
||||
XSK,
|
||||
"@alice:example.com",
|
||||
),
|
||||
user_signing: sign<ICrossSigningKey>(
|
||||
user_signing: sign<CrossSigningKeyInfo>(
|
||||
{
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
|
||||
@@ -136,11 +136,6 @@ describe("EventTimelineSet", () => {
|
||||
expect(eventsInLiveTimeline.length).toStrictEqual(1);
|
||||
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
|
||||
});
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
|
||||
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventToTimeline", () => {
|
||||
|
||||
@@ -386,6 +386,9 @@ describe("MatrixClient", function () {
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
|
||||
);
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true, true)).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true, true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1557,7 +1560,6 @@ describe("MatrixClient", function () {
|
||||
},
|
||||
},
|
||||
event_id: "$ev1",
|
||||
user_id: "@alice:matrix.org",
|
||||
});
|
||||
|
||||
expect(rootEvent.isThreadRoot).toBe(true);
|
||||
|
||||
@@ -16,11 +16,12 @@ limitations under the License.
|
||||
|
||||
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
|
||||
import { EventTimeline } from "../../../src/models/event-timeline";
|
||||
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
|
||||
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
|
||||
import { EncryptedFile } from "../../../src/@types/media";
|
||||
|
||||
describe("MSC3089Branch", () => {
|
||||
let client: MatrixClient;
|
||||
@@ -254,7 +255,7 @@ describe("MSC3089Branch", () => {
|
||||
it("should create new versions of itself", async () => {
|
||||
const canaryName = "canary";
|
||||
const canaryContents = "contents go here";
|
||||
const canaryFile = {} as IEncryptedFile;
|
||||
const canaryFile = {} as EncryptedFile;
|
||||
const canaryAddl = { canary: true };
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateKeyOrder = [fileEventId2, fileEventId];
|
||||
@@ -287,23 +288,21 @@ describe("MSC3089Branch", () => {
|
||||
|
||||
const createFn = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(name: string, contents: ArrayBuffer, info: Partial<IEncryptedFile>, addl: IContent) => {
|
||||
expect(name).toEqual(canaryName);
|
||||
expect(contents).toBe(canaryContents);
|
||||
expect(info).toBe(canaryFile);
|
||||
expect(addl).toMatchObject({
|
||||
...canaryAddl,
|
||||
"m.new_content": true,
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: fileEventId,
|
||||
},
|
||||
});
|
||||
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
|
||||
expect(name).toEqual(canaryName);
|
||||
expect(contents).toBe(canaryContents);
|
||||
expect(info).toBe(canaryFile);
|
||||
expect(addl).toMatchObject({
|
||||
...canaryAddl,
|
||||
"m.new_content": true,
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: fileEventId,
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId2 });
|
||||
},
|
||||
);
|
||||
return Promise.resolve({ event_id: fileEventId2 });
|
||||
});
|
||||
directory.createFile = createFn;
|
||||
|
||||
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
|
||||
|
||||
+282
-240
@@ -61,251 +61,293 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
let httpBackend: MockHttpBackend;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(async function () {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
httpBackend = new MockHttpBackend();
|
||||
/**
|
||||
* We need to split the tests into regular ones (these) and ones that use fake timers,
|
||||
* because the fake indexeddb uses timers too and appears make tests cause other tests
|
||||
* to fail if we keep enabling/disabling fake timers within the same test suite.
|
||||
*/
|
||||
describe("non-timed tests", () => {
|
||||
beforeEach(async function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
let store: IStore;
|
||||
if (storeType === StoreType.IndexedDB) {
|
||||
const idbStore = new IndexedDBStore({ indexedDB: fakeIndexedDB });
|
||||
await idbStore.startup();
|
||||
store = idbStore;
|
||||
} else {
|
||||
store = new MemoryStore();
|
||||
}
|
||||
let store: IStore;
|
||||
if (storeType === StoreType.IndexedDB) {
|
||||
const idbStore = new IndexedDBStore({ indexedDB: fakeIndexedDB });
|
||||
await idbStore.startup();
|
||||
store = idbStore;
|
||||
} else {
|
||||
store = new MemoryStore();
|
||||
}
|
||||
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jest.useRealTimers();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("sends a to-device message", async function () {
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual(EXPECTED_BODY);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
// let the code handle the response to the request so we don't get
|
||||
// log output after the test has finished (apparently stopping the
|
||||
// client in aftereach is not sufficient.)
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("retries on error", async function () {
|
||||
jest.useFakeTimers();
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual(EXPECTED_BODY);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("stops retrying on 4xx errors", async function () {
|
||||
jest.useFakeTimers();
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(400);
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// Asserting that another request is never made is obviously
|
||||
// a bit tricky - we just flush the queue what should hopefully
|
||||
// be plenty of times and assert that nothing comes through.
|
||||
let tries = 0;
|
||||
await flushAndRunTimersUntil(() => ++tries === 10);
|
||||
|
||||
expect(httpBackend.requests.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("honours ratelimiting", async function () {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// pick something obscure enough it's unlikley to clash with a
|
||||
// retry delay the algorithm uses anyway
|
||||
const retryDelay = 279 * 1000;
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(429, {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
retry_after_ms: retryDelay,
|
||||
});
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
logger.info("Advancing clock to just before expected retry time...");
|
||||
|
||||
jest.advanceTimersByTime(retryDelay - 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.requests.length).toEqual(0);
|
||||
|
||||
logger.info("Advancing clock past expected retry time...");
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on retryImmediately()", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.retryImmediately();
|
||||
|
||||
// longer timeout here to try & avoid flakiness
|
||||
expect(await httpBackend.flush(undefined, 1, 3000)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on when client is started", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.stopClient();
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries when a message is retried", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
const dummyEvent = new MatrixEvent({
|
||||
event_id: "!fake:example.org",
|
||||
});
|
||||
const mockRoom = {
|
||||
updatePendingEvent: jest.fn(),
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("splits many messages into multiple HTTP requests", async function () {
|
||||
const batch: ToDeviceBatch = {
|
||||
eventType: "org.example.foo",
|
||||
batch: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i <= 20; ++i) {
|
||||
batch.batch.push({
|
||||
userId: `@user${i}:example.org`,
|
||||
deviceId: FAKE_DEVICE_ID,
|
||||
payload: FAKE_PAYLOAD,
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const expectedCounts = [20, 1];
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(
|
||||
removeElement(expectedCounts, (c) => c === Object.keys(request.data.messages).length),
|
||||
).toBeTruthy();
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(Object.keys(request.data.messages).length).toEqual(1);
|
||||
})
|
||||
.respond(200, {});
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
await client.queueToDevice(batch);
|
||||
await httpBackend.flushAllExpected();
|
||||
it("sends a to-device message", async function () {
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual(EXPECTED_BODY);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
// let the code handle the response to the request so we don't get
|
||||
// log output after the test has finished (apparently stopping the
|
||||
// client in aftereach is not sufficient.)
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("retries on retryImmediately()", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.retryImmediately();
|
||||
|
||||
// longer timeout here to try & avoid flakiness
|
||||
expect(await httpBackend.flush(undefined, 1, 3000)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries on when client is started", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
client.stopClient();
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("retries when a message is retried", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
await client.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
});
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
const dummyEvent = new MatrixEvent({
|
||||
event_id: "!fake:example.org",
|
||||
});
|
||||
const mockRoom = {
|
||||
updatePendingEvent: jest.fn(),
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
});
|
||||
|
||||
it("splits many messages into multiple HTTP requests", async function () {
|
||||
const batch: ToDeviceBatch = {
|
||||
eventType: "org.example.foo",
|
||||
batch: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i <= 20; ++i) {
|
||||
batch.batch.push({
|
||||
userId: `@user${i}:example.org`,
|
||||
deviceId: FAKE_DEVICE_ID,
|
||||
payload: FAKE_PAYLOAD,
|
||||
});
|
||||
}
|
||||
|
||||
const expectedCounts = [20, 1];
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(
|
||||
removeElement(expectedCounts, (c) => c === Object.keys(request.data.messages).length),
|
||||
).toBeTruthy();
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(Object.keys(request.data.messages).length).toEqual(1);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await client.queueToDevice(batch);
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
describe("async tests", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
let store: IStore;
|
||||
if (storeType === StoreType.IndexedDB) {
|
||||
const idbStore = new IndexedDBStore({ indexedDB: fakeIndexedDB });
|
||||
let storeStarted = false;
|
||||
idbStore.startup().then(() => {
|
||||
storeStarted = true;
|
||||
});
|
||||
await flushAndRunTimersUntil(() => storeStarted);
|
||||
store = idbStore;
|
||||
} else {
|
||||
store = new MemoryStore();
|
||||
}
|
||||
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("retries on error", async function () {
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(500);
|
||||
|
||||
httpBackend
|
||||
.when("PUT", "/sendToDevice/org.example.foo/")
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual(EXPECTED_BODY);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
client
|
||||
.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
})
|
||||
.then();
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// flush, as per comment in first test
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("stops retrying on 4xx errors", async function () {
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(400);
|
||||
|
||||
client
|
||||
.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
})
|
||||
.then();
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
|
||||
// Asserting that another request is never made is obviously
|
||||
// a bit tricky - we just flush the queue what should hopefully
|
||||
// be plenty of times and assert that nothing comes through.
|
||||
let tries = 0;
|
||||
await flushAndRunTimersUntil(() => ++tries === 10);
|
||||
|
||||
expect(httpBackend.requests.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("honours ratelimiting", async function () {
|
||||
// pick something obscure enough it's unlikley to clash with a
|
||||
// retry delay the algorithm uses anyway
|
||||
const retryDelay = 279 * 1000;
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(429, {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
retry_after_ms: retryDelay,
|
||||
});
|
||||
|
||||
httpBackend.when("PUT", "/sendToDevice/org.example.foo/").respond(200, {});
|
||||
|
||||
client
|
||||
.queueToDevice({
|
||||
eventType: "org.example.foo",
|
||||
batch: [FAKE_MSG],
|
||||
})
|
||||
.then();
|
||||
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
logger.info("Advancing clock to just before expected retry time...");
|
||||
|
||||
jest.advanceTimersByTime(retryDelay - 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.requests.length).toEqual(0);
|
||||
|
||||
logger.info("Advancing clock past expected retry time...");
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
await flushPromises();
|
||||
|
||||
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
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 { ClientPrefix, IHttpOpts, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { ClientRendezvousFailureReason, MSC4108RendezvousSession } from "../../../src/rendezvous";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
prefix: ClientPrefix.Unstable,
|
||||
onlyData: true,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
fetchMock.config.overwriteRoutes = true;
|
||||
|
||||
describe("MSC4108RendezvousSession", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
async function postAndCheckLocation(msc4108Enabled: boolean, fallbackRzServer: string, locationResponse: string) {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled });
|
||||
const transport = new MSC4108RendezvousSession({ client, fallbackRzServer });
|
||||
{
|
||||
// initial POST
|
||||
const expectedPostLocation = msc4108Enabled
|
||||
? `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc4108/rendezvous`
|
||||
: fallbackRzServer;
|
||||
|
||||
fetchMock.postOnce(expectedPostLocation, {
|
||||
status: 201,
|
||||
body: { url: locationResponse },
|
||||
});
|
||||
await transport.send("data");
|
||||
}
|
||||
|
||||
{
|
||||
fetchMock.get(locationResponse, {
|
||||
status: 200,
|
||||
body: "data",
|
||||
headers: {
|
||||
"content-type": "text/plain",
|
||||
"etag": "aaa",
|
||||
},
|
||||
});
|
||||
await expect(transport.receive()).resolves.toEqual("data");
|
||||
}
|
||||
}
|
||||
|
||||
it("should use custom fetchFn if provided", async () => {
|
||||
const sandbox = fetchMock.sandbox();
|
||||
const fetchFn = jest.fn().mockImplementation(sandbox);
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fetchFn,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
sandbox.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: {
|
||||
url: "https://fallbackserver/rz/123",
|
||||
},
|
||||
});
|
||||
await transport.send("data");
|
||||
await sandbox.flush(true);
|
||||
expect(fetchFn).toHaveBeenCalledWith("https://fallbackserver/rz", expect.anything());
|
||||
});
|
||||
|
||||
it("should throw an error when no server available", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({ client });
|
||||
await expect(transport.send("data")).rejects.toThrow("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://fallbackserver/rz/123" },
|
||||
});
|
||||
await fetchMock.flush(true);
|
||||
await expect(transport.send("data")).resolves.toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST with no location", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
});
|
||||
await Promise.all([expect(transport.send("data")).rejects.toThrow(), fetchMock.flush(true)]);
|
||||
});
|
||||
|
||||
it("POST with absolute path response", async function () {
|
||||
await postAndCheckLocation(false, "https://fallbackserver/rz", "https://fallbackserver/123");
|
||||
});
|
||||
|
||||
it("POST to built-in MSC3886 implementation", async function () {
|
||||
await postAndCheckLocation(
|
||||
true,
|
||||
"https://fallbackserver/rz",
|
||||
"https://example.com/_matrix/client/unstable/org.matrix.msc4108/rendezvous/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function () {
|
||||
await postAndCheckLocation(false, "https://fallbackserver/rz/abc", "https://fallbackserver/rz/xyz/123");
|
||||
});
|
||||
|
||||
// fetch-mock doesn't handle redirects properly, so we can't test this
|
||||
it.skip("POST to follow 307 to other server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 307,
|
||||
redirectUrl: "https://redirected.fallbackserver/rz",
|
||||
redirected: true,
|
||||
});
|
||||
fetchMock.postOnce("https://redirected.fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://redirected.fallbackserver/rz/123" },
|
||||
headers: { etag: "aaa" },
|
||||
});
|
||||
await fetchMock.flush(true);
|
||||
await expect(transport.send("data")).resolves.toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST and GET", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://fallbackserver/rz/123" },
|
||||
});
|
||||
await expect(transport.send("foo=baa")).resolves.toStrictEqual(undefined);
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock).toHaveFetched("https://fallbackserver/rz", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
functionMatcher: (_, opts): boolean => {
|
||||
return opts.body === "foo=baa";
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
// first GET without etag
|
||||
fetchMock.getOnce("https://fallbackserver/rz/123", {
|
||||
status: 200,
|
||||
body: "foo=baa",
|
||||
headers: { "content-type": "text/plain", "etag": "aaa" },
|
||||
});
|
||||
await expect(transport.receive()).resolves.toEqual("foo=baa");
|
||||
await fetchMock.flush(true);
|
||||
}
|
||||
{
|
||||
// subsequent GET which should have etag from previous request
|
||||
fetchMock.getOnce("https://fallbackserver/rz/123", {
|
||||
status: 200,
|
||||
body: "foo=baa",
|
||||
headers: { "content-type": "text/plain", "etag": "bbb" },
|
||||
});
|
||||
await expect(transport.receive()).resolves.toEqual("foo=baa");
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock).toHaveFetched("https://fallbackserver/rz/123", {
|
||||
method: "GET",
|
||||
headers: { "if-none-match": "aaa" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and PUTs", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://fallbackserver/rz/123" },
|
||||
headers: { etag: "aaa" },
|
||||
});
|
||||
await transport.send("foo=baa");
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock).toHaveFetched("https://fallbackserver/rz", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
functionMatcher: (_, opts): boolean => {
|
||||
return opts.body === "foo=baa";
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
// subsequent PUT which should have etag from previous request
|
||||
fetchMock.putOnce("https://fallbackserver/rz/123", { status: 202, headers: { etag: "bbb" } });
|
||||
await transport.send("c=d");
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock).toHaveFetched("https://fallbackserver/rz/123", {
|
||||
method: "PUT",
|
||||
headers: { "if-match": "aaa" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and DELETE", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
{
|
||||
// Create
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://fallbackserver/rz/123" },
|
||||
});
|
||||
await expect(transport.send("foo=baa")).resolves.toStrictEqual(undefined);
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock).toHaveFetched("https://fallbackserver/rz", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "text/plain" },
|
||||
functionMatcher: (_, opts): boolean => {
|
||||
return opts.body === "foo=baa";
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
// Cancel
|
||||
fetchMock.deleteOnce("https://fallbackserver/rz/123", { status: 204 });
|
||||
await transport.cancel(ClientRendezvousFailureReason.UserDeclined);
|
||||
await fetchMock.flush(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("send after cancelled", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
await transport.cancel(ClientRendezvousFailureReason.UserDeclined);
|
||||
await expect(transport.send("data")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("receive before ready", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
});
|
||||
await expect(transport.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
onFailure,
|
||||
});
|
||||
|
||||
fetchMock.postOnce("https://fallbackserver/rz", { status: 404 });
|
||||
await Promise.all([expect(transport.send("foo=baa")).resolves.toBeUndefined(), fetchMock.flush(true)]);
|
||||
expect(onFailure).toHaveBeenCalledWith(ClientRendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc4108Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
onFailure,
|
||||
});
|
||||
|
||||
{
|
||||
// initial POST
|
||||
fetchMock.postOnce("https://fallbackserver/rz", {
|
||||
status: 201,
|
||||
body: { url: "https://fallbackserver/rz/123" },
|
||||
headers: { expires: "Thu, 01 Jan 1970 00:00:00 GMT" },
|
||||
});
|
||||
|
||||
await transport.send("foo=baa");
|
||||
await fetchMock.flush(true);
|
||||
}
|
||||
{
|
||||
// GET with 404 to simulate expiry
|
||||
fetchMock.getOnce("https://fallbackserver/rz/123", { status: 404, body: "foo=baa" });
|
||||
await Promise.all([expect(transport.receive()).resolves.toBeUndefined(), fetchMock.flush(true)]);
|
||||
expect(onFailure).toHaveBeenCalledWith(ClientRendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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 { EstablishedEcies, QrCodeData, QrCodeMode, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous";
|
||||
|
||||
describe("MSC4108SecureChannel", () => {
|
||||
const baseUrl = "https://example.com";
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
|
||||
it("should generate qr code data as expected", async () => {
|
||||
const session = new MSC4108RendezvousSession({
|
||||
url,
|
||||
});
|
||||
const channel = new MSC4108SecureChannel(session);
|
||||
|
||||
const code = await channel.generateCode(QrCodeMode.Login);
|
||||
expect(code).toHaveLength(71);
|
||||
const text = new TextDecoder().decode(code);
|
||||
expect(text.startsWith("MATRIX")).toBeTruthy();
|
||||
expect(text.endsWith(url)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should throw error if attempt to connect multiple times", async () => {
|
||||
const mockSession = {
|
||||
send: jest.fn(),
|
||||
receive: jest.fn(),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const channel = new MSC4108SecureChannel(mockSession);
|
||||
|
||||
const qrCodeData = QrCodeData.from_bytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
||||
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||
qrCodeData.public_key,
|
||||
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||
);
|
||||
mocked(mockSession.receive).mockResolvedValue(ciphertext);
|
||||
await channel.connect();
|
||||
await expect(channel.connect()).rejects.toThrow("Channel already connected");
|
||||
});
|
||||
|
||||
it("should throw error on invalid initiate response", async () => {
|
||||
const mockSession = {
|
||||
send: jest.fn(),
|
||||
receive: jest.fn(),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const channel = new MSC4108SecureChannel(mockSession);
|
||||
|
||||
mocked(mockSession.receive).mockResolvedValue("");
|
||||
await expect(channel.connect()).rejects.toThrow("No response from other device");
|
||||
|
||||
const qrCodeData = QrCodeData.from_bytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
||||
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||
qrCodeData.public_key,
|
||||
"NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||
);
|
||||
|
||||
mocked(mockSession.receive).mockResolvedValue(ciphertext);
|
||||
await expect(channel.connect()).rejects.toThrow("Invalid response from other device");
|
||||
});
|
||||
|
||||
describe("should be able to connect as a reciprocating device", () => {
|
||||
let mockSession: MSC4108RendezvousSession;
|
||||
let channel: MSC4108SecureChannel;
|
||||
let opponentChannel: EstablishedEcies;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockSession = {
|
||||
send: jest.fn(),
|
||||
receive: jest.fn(),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
channel = new MSC4108SecureChannel(mockSession);
|
||||
|
||||
const qrCodeData = QrCodeData.from_bytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
||||
const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||
qrCodeData.public_key,
|
||||
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||
);
|
||||
opponentChannel = _opponentChannel;
|
||||
|
||||
mocked(mockSession.receive).mockResolvedValue(ciphertext);
|
||||
await channel.connect();
|
||||
expect(opponentChannel.decrypt(mocked(mockSession.send).mock.calls[0][0])).toBe("MATRIX_QR_CODE_LOGIN_OK");
|
||||
mocked(mockSession.send).mockReset();
|
||||
});
|
||||
|
||||
it("should be able to securely send encrypted payloads", async () => {
|
||||
const payload = {
|
||||
type: PayloadType.Secrets,
|
||||
protocols: ["a", "b", "c"],
|
||||
homeserver: "https://example.org",
|
||||
};
|
||||
await channel.secureSend(payload);
|
||||
expect(mockSession.send).toHaveBeenCalled();
|
||||
expect(opponentChannel.decrypt(mocked(mockSession.send).mock.calls[0][0])).toBe(JSON.stringify(payload));
|
||||
});
|
||||
|
||||
it("should be able to securely receive encrypted payloads", async () => {
|
||||
const payload = {
|
||||
type: PayloadType.Secrets,
|
||||
protocols: ["a", "b", "c"],
|
||||
homeserver: "https://example.org",
|
||||
};
|
||||
const ciphertext = opponentChannel.encrypt(JSON.stringify(payload));
|
||||
mocked(mockSession.receive).mockResolvedValue(ciphertext);
|
||||
await expect(channel.secureReceive()).resolves.toEqual(payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../olm-loader";
|
||||
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
|
||||
@@ -17,7 +17,12 @@ limitations under the License.
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import "../../olm-loader";
|
||||
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
RendezvousCode,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv2RendezvousCode as ECDHRendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { MatrixClient } from "../../../src";
|
||||
import { RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc3886Enabled: boolean }): MatrixClient {
|
||||
|
||||
+2
-21
@@ -22,7 +22,7 @@ import { mocked } from "jest-mock";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, Optional, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { emitPromise, IMessageOpts } from "../test-utils/test-utils";
|
||||
import {
|
||||
Direction,
|
||||
DuplicateStrategy,
|
||||
@@ -54,7 +54,6 @@ import { Crypto } from "../../src/crypto";
|
||||
import * as threadUtils from "../test-utils/thread";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { IMessageOpts } from "../test-utils/test-utils";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
@@ -339,24 +338,6 @@ describe("Room", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", async () => {
|
||||
await expect(room.addLiveEvents(events, DuplicateStrategy.Replace, false)).resolves.not.toThrow();
|
||||
await expect(room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).resolves.not.toThrow();
|
||||
await expect(
|
||||
// @ts-ignore
|
||||
room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", async function () {
|
||||
return expect(
|
||||
// @ts-ignore
|
||||
room.addLiveEvents(events, {
|
||||
duplicateStrategy: "foo",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should replace a timeline event if dupe strategy is 'replace'", async function () {
|
||||
// make a duplicate
|
||||
const dupe = utils.mkMessage({
|
||||
@@ -387,7 +368,7 @@ describe("Room", function () {
|
||||
expect(room.timeline[0]).toEqual(events[0]);
|
||||
// @ts-ignore
|
||||
await room.addLiveEvents([dupe], {
|
||||
duplicateStrategy: "ignore",
|
||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||
});
|
||||
expect(room.timeline[0]).toEqual(events[0]);
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("initRustCrypto", () => {
|
||||
} as unknown as Mocked<OlmMachine>;
|
||||
}
|
||||
|
||||
it("passes through the store params", async () => {
|
||||
it("passes through the store params (passphrase)", async () => {
|
||||
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
|
||||
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
|
||||
|
||||
@@ -126,7 +126,30 @@ describe("initRustCrypto", () => {
|
||||
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
|
||||
});
|
||||
|
||||
it("suppresses the storePassphrase if storePrefix is unset", async () => {
|
||||
it("passes through the store params (key)", async () => {
|
||||
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
|
||||
jest.spyOn(StoreHandle, "openWithKey").mockResolvedValue(mockStore);
|
||||
|
||||
const testOlmMachine = makeTestOlmMachine();
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
|
||||
const storeKey = new Uint8Array(32);
|
||||
await initRustCrypto({
|
||||
logger,
|
||||
http: {} as MatrixClient["http"],
|
||||
userId: TEST_USER,
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
secretStorage: {} as ServerSideSecretStorage,
|
||||
cryptoCallbacks: {} as CryptoCallbacks,
|
||||
storePrefix: "storePrefix",
|
||||
storeKey: storeKey,
|
||||
});
|
||||
|
||||
expect(StoreHandle.openWithKey).toHaveBeenCalledWith("storePrefix", storeKey);
|
||||
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
|
||||
});
|
||||
|
||||
it("suppresses the storePassphrase and storeKey if storePrefix is unset", async () => {
|
||||
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
|
||||
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
|
||||
|
||||
@@ -141,10 +164,11 @@ describe("initRustCrypto", () => {
|
||||
secretStorage: {} as ServerSideSecretStorage,
|
||||
cryptoCallbacks: {} as CryptoCallbacks,
|
||||
storePrefix: null,
|
||||
storeKey: new Uint8Array(),
|
||||
storePassphrase: "storePassphrase",
|
||||
});
|
||||
|
||||
expect(StoreHandle.open).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(StoreHandle.open).toHaveBeenCalledWith();
|
||||
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
|
||||
});
|
||||
|
||||
@@ -1395,14 +1419,15 @@ describe("RustCrypto", () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
|
||||
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
|
||||
await olmMachine.enableBackupV1(
|
||||
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
backupVersion,
|
||||
);
|
||||
|
||||
// we import two keys: one "from backup", and one "from export"
|
||||
const [backedUpRoomKey, exportedRoomKey] = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey]);
|
||||
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey], backupVersion);
|
||||
await rustCrypto.importRoomKeys([exportedRoomKey]);
|
||||
|
||||
// we ask for the keys that should be backed up
|
||||
@@ -1437,16 +1462,17 @@ describe("RustCrypto", () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
|
||||
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
|
||||
await olmMachine.enableBackupV1(
|
||||
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
backupVersion,
|
||||
);
|
||||
|
||||
const backup = Array.from(testData.MEGOLM_SESSION_DATA_ARRAY);
|
||||
// in addition to correct keys, we restore an invalid key
|
||||
backup.push({ room_id: "!roomid", session_id: "sessionid" } as IMegolmSessionData);
|
||||
const progressCallback = jest.fn();
|
||||
await rustCrypto.importBackedUpRoomKeys(backup, { progressCallback });
|
||||
await rustCrypto.importBackedUpRoomKeys(backup, backupVersion, { progressCallback });
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: 3,
|
||||
successes: 0,
|
||||
@@ -1489,6 +1515,44 @@ describe("RustCrypto", () => {
|
||||
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("import & export secrets bundle", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await makeTestRustCrypto(
|
||||
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||
baseUrl: "http://server/",
|
||||
prefix: "",
|
||||
onlyData: true,
|
||||
}),
|
||||
testData.TEST_USER_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if there is nothing to export", async () => {
|
||||
await expect(rustCrypto.exportSecretsBundle()).rejects.toThrow(
|
||||
"The store doesn't contain any cross-signing keys",
|
||||
);
|
||||
});
|
||||
|
||||
it("should correctly import & export a secrets bundle", async () => {
|
||||
const bundle = {
|
||||
cross_signing: {
|
||||
master_key: "bMnVpkHI4S2wXRxy+IpaKM5PIAUUkl6DE+n0YLIW/qs",
|
||||
user_signing_key: "8tlgLjUrrb/zGJo4YKGhDTIDCEjtJTAS/Sh2AGNLuIo",
|
||||
self_signing_key: "pfDknmP5a0fVVRE54zhkUgJfzbNmvKcNfIWEW796bQs",
|
||||
},
|
||||
backup: {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
key: "bYYv3aFLQ49jMNcOjuTtBY9EKDby2x1m3gfX81nIKRQ",
|
||||
backup_version: "9",
|
||||
},
|
||||
};
|
||||
await rustCrypto.importSecretsBundle(bundle);
|
||||
await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Build a MatrixHttpApi instance */
|
||||
|
||||
@@ -147,11 +147,6 @@ export interface LoginRequest {
|
||||
* The login type being used.
|
||||
*/
|
||||
type: "m.login.password" | "m.login.token" | string;
|
||||
/**
|
||||
* Third-party identifier for the user.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
address?: string;
|
||||
/**
|
||||
* ID of the client device.
|
||||
* If this does not correspond to a known client device, a new device will be created.
|
||||
|
||||
+2
-8
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { NamespacedValue, UnstableValue } from "../NamespacedValue";
|
||||
import {
|
||||
PolicyRuleEventContent,
|
||||
RoomAvatarEventContent,
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
SDPStreamMetadataKey,
|
||||
} from "../webrtc/callEventTypes";
|
||||
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
|
||||
import { EncryptedFile } from "./media";
|
||||
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
|
||||
|
||||
export enum EventType {
|
||||
@@ -303,12 +302,7 @@ export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matr
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const UNSIGNED_MEMBERSHIP_FIELD = new UnstableValue("membership", "io.element.msc4115.membership");
|
||||
|
||||
/**
|
||||
* @deprecated in favour of {@link EncryptedFile}
|
||||
*/
|
||||
export type IEncryptedFile = EncryptedFile;
|
||||
export const UNSIGNED_MEMBERSHIP_FIELD = new NamespacedValue("membership", "io.element.msc4115.membership");
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified non-state room events.
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 type * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
declare module "@matrix-org/matrix-sdk-crypto-wasm" {
|
||||
interface OlmMachine {
|
||||
importSecretsBundle(bundle: RustSdkCryptoJs.SecretsBundle): Promise<void>;
|
||||
exportSecretsBundle(): Promise<RustSdkCryptoJs.SecretsBundle>;
|
||||
}
|
||||
|
||||
interface SecretsBundle {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
to_json(): Promise<{
|
||||
cross_signing: {
|
||||
master_key: string;
|
||||
self_signing_key: string;
|
||||
user_signing_key: string;
|
||||
};
|
||||
backup?: {
|
||||
algorithm: string;
|
||||
key: string;
|
||||
backup_version: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,6 @@ export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership",
|
||||
}
|
||||
|
||||
export interface IJoinRuleEventContent {
|
||||
join_rule: JoinRule; // eslint-disable-line camelcase
|
||||
allow?: {
|
||||
type: RestrictedAllowType;
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
|
||||
@@ -52,21 +52,13 @@ export interface RegisterRequest {
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
* Guest users can also upgrade their account by going through the ordinary register flow,
|
||||
* but specifying the additional POST parameter guest_access_token containing the guest’s access token.
|
||||
* They are also required to specify the username parameter to the value of the local part of their username,
|
||||
* which is otherwise optional.
|
||||
* @see https://spec.matrix.org/v1.10/client-server-api/#guest-access
|
||||
*/
|
||||
guest_access_token?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
x_show_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,10 +99,4 @@ export interface RegisterResponse {
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
*
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ enum GroupKey {
|
||||
}
|
||||
|
||||
export interface IResultRoomEvents {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
results: ISearchResult[];
|
||||
count?: number;
|
||||
highlights?: string[];
|
||||
results?: ISearchResult[];
|
||||
state?: { [roomId: string]: IStateEventWithRoomId[] };
|
||||
groups?: {
|
||||
[groupKey in GroupKey]: {
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { RoomType } from "./event";
|
||||
import { GuestAccess, HistoryVisibility, RestrictedAllowType } from "./partials";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials";
|
||||
import { ImageInfo } from "./media";
|
||||
import { PolicyRecommendation } from "../models/invites-ignorer";
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface RoomCreateEventContent {
|
||||
}
|
||||
|
||||
export interface RoomJoinRulesEventContent {
|
||||
join_rule: JoinRule;
|
||||
allow?: {
|
||||
room_id: string;
|
||||
type: RestrictedAllowType;
|
||||
|
||||
@@ -43,8 +43,6 @@ export enum AutoDiscoveryError {
|
||||
InvalidJson = "Invalid JSON",
|
||||
UnsupportedHomeserverSpecVersion = "The homeserver does not meet the version requirements",
|
||||
|
||||
/** @deprecated Replaced by `UnsupportedHomeserverSpecVersion` */
|
||||
HomeserverTooOld = UnsupportedHomeserverSpecVersion,
|
||||
// TODO: Implement when Sydent supports the `/versions` endpoint - https://github.com/matrix-org/sydent/issues/424
|
||||
//IdentityServerTooOld = "The identity server does not meet the minimum version requirements",
|
||||
}
|
||||
@@ -91,9 +89,6 @@ export class AutoDiscovery {
|
||||
public static readonly ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION =
|
||||
AutoDiscoveryError.UnsupportedHomeserverSpecVersion;
|
||||
|
||||
/** @deprecated Replaced by ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION */
|
||||
public static readonly ERROR_HOMESERVER_TOO_OLD = AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION;
|
||||
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError) as AutoDiscoveryError[];
|
||||
|
||||
/**
|
||||
|
||||
+60
-63
@@ -357,14 +357,14 @@ export interface ICreateClientOpts {
|
||||
deviceToImport?: IExportedDevice;
|
||||
|
||||
/**
|
||||
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
|
||||
* Encryption key used for encrypting sensitive data (such as e2ee keys) in {@link ICreateClientOpts#cryptoStore}.
|
||||
*
|
||||
* This must be set to the same value every time the client is initialised for the same device.
|
||||
*
|
||||
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
|
||||
*
|
||||
* No particular requirement is placed on the key data (it is fed into an HKDF to generate the actual encryption
|
||||
* keys).
|
||||
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
|
||||
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
|
||||
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
|
||||
* data can be migrated from the legacy store.
|
||||
*/
|
||||
pickleKey?: string;
|
||||
|
||||
@@ -560,16 +560,13 @@ export interface Capabilities {
|
||||
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
|
||||
}
|
||||
|
||||
/** @deprecated prefer {@link CrossSigningKeyInfo}. */
|
||||
export type ICrossSigningKey = CrossSigningKeyInfo;
|
||||
|
||||
enum CrossSigningKeyType {
|
||||
MasterKey = "master_key",
|
||||
SelfSigningKey = "self_signing_key",
|
||||
UserSigningKey = "user_signing_key",
|
||||
}
|
||||
|
||||
export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
|
||||
export type CrossSigningKeys = Record<CrossSigningKeyType, CrossSigningKeyInfo>;
|
||||
|
||||
export type SendToDeviceContentMap = Map<string, Map<string, Record<string, any>>>;
|
||||
|
||||
@@ -581,7 +578,7 @@ export interface ISignedKey {
|
||||
device_id: string;
|
||||
}
|
||||
|
||||
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
|
||||
export type KeySignatures = Record<string, Record<string, CrossSigningKeyInfo | ISignedKey>>;
|
||||
export interface IUploadKeySignaturesResponse {
|
||||
failures: Record<
|
||||
string,
|
||||
@@ -1483,13 +1480,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
this.on(ClientEvent.Sync, this.startMatrixRTC);
|
||||
|
||||
// backwards compat for when 'opts' was 'historyLen'.
|
||||
if (typeof opts === "number") {
|
||||
opts = {
|
||||
initialSyncLimit: opts,
|
||||
};
|
||||
}
|
||||
|
||||
// Create our own user object artificially (instead of waiting for sync)
|
||||
// so it's always available, even if the user is not in any rooms etc.
|
||||
const userId = this.getUserId();
|
||||
@@ -2232,17 +2222,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* An alternative to {@link initCrypto}.
|
||||
*
|
||||
* *WARNING*: this API is very experimental, should not be used in production, and may change without notice!
|
||||
* Eventually it will be deprecated and `initCrypto` will do the same thing.
|
||||
*
|
||||
* @experimental
|
||||
*
|
||||
* @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
|
||||
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
|
||||
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
|
||||
* 32 bytes of data, and must be the same each time the client is initialised for a given device.
|
||||
* If both this and `storagePassword` are unspecified, the store will be unencrypted.
|
||||
* @param args.storagePassword - An alternative to `storageKey`. A password which will be used to derive a key to
|
||||
* encrypt the store with. Deriving a key from a password is (deliberately) a slow operation, so prefer
|
||||
* to pass a `storageKey` directly where possible.
|
||||
*
|
||||
* @returns a Promise which will resolve when the crypto layer has been
|
||||
* successfully initialised.
|
||||
*/
|
||||
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
|
||||
public async initRustCrypto(
|
||||
args: {
|
||||
useIndexedDB?: boolean;
|
||||
storageKey?: Uint8Array;
|
||||
storagePassword?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
if (this.cryptoBackend) {
|
||||
this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
|
||||
return;
|
||||
@@ -2275,11 +2272,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
deviceId: deviceId,
|
||||
secretStorage: this.secretStorage,
|
||||
cryptoCallbacks: this.cryptoCallbacks,
|
||||
storePrefix: useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
|
||||
storePassphrase: this.pickleKey,
|
||||
storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX,
|
||||
storeKey: args.storageKey,
|
||||
|
||||
// temporary compatibility hack: if there is no storageKey nor storagePassword, fall back to the pickleKey
|
||||
storePassphrase: args.storagePassword ?? this.pickleKey,
|
||||
|
||||
legacyCryptoStore: this.cryptoStore,
|
||||
legacyPickleKey: this.pickleKey ?? "DEFAULT_KEY",
|
||||
legacyMigrationProgressListener: (progress, total) => {
|
||||
legacyMigrationProgressListener: (progress: number, total: number): void => {
|
||||
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
|
||||
},
|
||||
});
|
||||
@@ -2898,7 +2899,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param event - event to be checked
|
||||
* @returns The event information.
|
||||
* @deprecated Prefer {@link CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
|
||||
* @deprecated Prefer {@link Crypto.CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
|
||||
*/
|
||||
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -3316,7 +3317,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
* @deprecated Prefer {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
|
||||
if (!this.crypto) {
|
||||
@@ -3373,7 +3374,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* the server, otherwise false. If we haven't completed a successful check
|
||||
* of key backup status yet, returns null.
|
||||
*
|
||||
* @deprecated Prefer direct access to {@link CryptoApi.getActiveSessionBackupVersion}:
|
||||
* @deprecated Prefer direct access to {@link Crypto.CryptoApi.getActiveSessionBackupVersion}:
|
||||
*
|
||||
* ```javascript
|
||||
* let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null;
|
||||
@@ -3393,7 +3394,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param info - Backup information object as returned by getKeyBackupVersion
|
||||
* @returns Promise which resolves when complete.
|
||||
*
|
||||
* @deprecated Do not call this directly. Instead call {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
* @deprecated Do not call this directly. Instead call {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
@@ -3855,12 +3856,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!backupInfo.version) {
|
||||
throw new Error("Backup version must be defined");
|
||||
}
|
||||
const backupVersion = backupInfo.version!;
|
||||
|
||||
let totalKeyCount = 0;
|
||||
let totalFailures = 0;
|
||||
let totalImported = 0;
|
||||
|
||||
const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version);
|
||||
const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupVersion);
|
||||
|
||||
const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey);
|
||||
|
||||
@@ -3874,7 +3876,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// Cache the key, if possible.
|
||||
// This is async.
|
||||
this.cryptoBackend
|
||||
.storeSessionBackupPrivateKey(privKey, backupInfo.version)
|
||||
.storeSessionBackupPrivateKey(privKey, backupVersion)
|
||||
.catch((e) => {
|
||||
this.logger.warn("Error caching session backup key:", e);
|
||||
})
|
||||
@@ -3914,7 +3916,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
async (chunk) => {
|
||||
// We have a chunk of decrypted keys: import them
|
||||
try {
|
||||
await this.cryptoBackend!.importBackedUpRoomKeys(chunk, {
|
||||
const backupVersion = backupInfo.version!;
|
||||
await this.cryptoBackend!.importBackedUpRoomKeys(chunk, backupVersion, {
|
||||
untrusted,
|
||||
});
|
||||
totalImported += chunk.length;
|
||||
@@ -3944,7 +3947,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
for (const k of keys) {
|
||||
k.room_id = targetRoomId!;
|
||||
}
|
||||
await this.cryptoBackend.importBackedUpRoomKeys(keys, {
|
||||
await this.cryptoBackend.importBackedUpRoomKeys(keys, backupVersion, {
|
||||
progressCallback,
|
||||
untrusted,
|
||||
});
|
||||
@@ -3958,7 +3961,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
key.room_id = targetRoomId!;
|
||||
key.session_id = targetSessionId!;
|
||||
|
||||
await this.cryptoBackend.importBackedUpRoomKeys([key], {
|
||||
await this.cryptoBackend.importBackedUpRoomKeys([key], backupVersion, {
|
||||
progressCallback,
|
||||
untrusted,
|
||||
});
|
||||
@@ -4876,10 +4879,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
|
||||
}
|
||||
path = utils.encodeUri(pathTemplate, pathParams);
|
||||
} else if (event.isRedaction()) {
|
||||
} else if (event.isRedaction() && event.event.redacts) {
|
||||
const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
|
||||
path = utils.encodeUri(pathTemplate, {
|
||||
$redactsEventId: event.event.redacts!,
|
||||
$redactsEventId: event.event.redacts,
|
||||
...pathParams,
|
||||
});
|
||||
} else {
|
||||
@@ -5775,7 +5778,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* anyone they share a room with. If false, will return null for such URLs.
|
||||
* @param allowRedirects - If true, the caller supports the URL being 307 or
|
||||
* 308 redirected to another resource upon request. If false, redirects
|
||||
* are not expected.
|
||||
* are not expected. Implied `true` when `useAuthentication` is `true`.
|
||||
* @param useAuthentication - If true, the caller supports authenticated
|
||||
* media and wants an authentication-required URL. Note that server support
|
||||
* for authenticated media will *not* be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that `useAuthentication`
|
||||
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
|
||||
* @returns the avatar URL or null.
|
||||
*/
|
||||
public mxcUrlToHttp(
|
||||
@@ -5785,8 +5793,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks?: boolean,
|
||||
allowRedirects?: boolean,
|
||||
useAuthentication?: boolean,
|
||||
): string | null {
|
||||
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks, allowRedirects);
|
||||
return getHttpUriForMxc(
|
||||
this.baseUrl,
|
||||
mxcUrl,
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
allowDirectLinks,
|
||||
allowRedirects,
|
||||
useAuthentication,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7091,7 +7109,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// append the new results to our existing results
|
||||
const resultsLength = roomEvents.results?.length ?? 0;
|
||||
for (let i = 0; i < resultsLength; i++) {
|
||||
const sr = SearchResult.fromJson(roomEvents.results[i], mapper);
|
||||
const sr = SearchResult.fromJson(roomEvents.results![i], mapper);
|
||||
const room = this.getRoom(sr.context.getEvent().getRoomId());
|
||||
if (room) {
|
||||
// Copy over a known event sender if we can
|
||||
@@ -7876,16 +7894,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
password: string,
|
||||
sessionId: string | null,
|
||||
auth: { session?: string; type: string },
|
||||
bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
|
||||
bindThreepids?: { email?: boolean; msisdn?: boolean },
|
||||
guestAccessToken?: string,
|
||||
inhibitLogin?: boolean,
|
||||
): Promise<RegisterResponse> {
|
||||
// backwards compat
|
||||
if (bindThreepids === true) {
|
||||
bindThreepids = { email: true };
|
||||
} else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
|
||||
bindThreepids = {};
|
||||
}
|
||||
if (sessionId) {
|
||||
auth.session = sessionId;
|
||||
}
|
||||
@@ -7900,27 +7912,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (password !== undefined && password !== null) {
|
||||
params.password = password;
|
||||
}
|
||||
if (bindThreepids.email) {
|
||||
params.bind_email = true;
|
||||
}
|
||||
if (bindThreepids.msisdn) {
|
||||
params.bind_msisdn = true;
|
||||
}
|
||||
if (guestAccessToken !== undefined && guestAccessToken !== null) {
|
||||
params.guest_access_token = guestAccessToken;
|
||||
}
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) {
|
||||
params.inhibit_login = inhibitLogin;
|
||||
}
|
||||
// Temporary parameter added to make the register endpoint advertise
|
||||
// msisdn flows. This exists because there are clients that break
|
||||
// when given stages they don't recognise. This parameter will cease
|
||||
// to be necessary once these old clients are gone.
|
||||
// Only send it if we send any params at all (the password param is
|
||||
// mandatory, so if we send any params, we'll send the password param)
|
||||
if (password !== undefined && password !== null) {
|
||||
params.x_show_msisdn = true;
|
||||
}
|
||||
|
||||
return this.registerRequest(params);
|
||||
}
|
||||
|
||||
@@ -113,10 +113,11 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* Import a list of room keys restored from backup
|
||||
*
|
||||
* @param keys - a list of session export objects
|
||||
* @param backupVersion - the version of the backup these keys came from.
|
||||
* @param opts - options object
|
||||
* @returns a promise which resolves once the keys have been imported
|
||||
*/
|
||||
importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
|
||||
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api
|
||||
|
||||
+29
-3
@@ -30,7 +30,12 @@ import { encodeParams } from "./utils";
|
||||
* for such URLs.
|
||||
* @param allowRedirects - If true, the caller supports the URL being 307 or
|
||||
* 308 redirected to another resource upon request. If false, redirects
|
||||
* are not expected.
|
||||
* are not expected. Implied `true` when `useAuthentication` is `true`.
|
||||
* @param useAuthentication - If true, the caller supports authenticated
|
||||
* media and wants an authentication-required URL. Note that server support
|
||||
* for authenticated media will *not* be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that `useAuthentication`
|
||||
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
|
||||
* @returns The complete URL to the content.
|
||||
*/
|
||||
export function getHttpUriForMxc(
|
||||
@@ -41,6 +46,7 @@ export function getHttpUriForMxc(
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks = false,
|
||||
allowRedirects?: boolean,
|
||||
useAuthentication?: boolean,
|
||||
): string {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return "";
|
||||
@@ -52,8 +58,23 @@ export function getHttpUriForMxc(
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (useAuthentication) {
|
||||
allowRedirects = true; // per docs (MSC3916 always expects redirects)
|
||||
|
||||
// Dev note: MSC3916 (as of writing) removes `allow_redirect` entirely, but
|
||||
// for explicitness we set it here. This makes it slightly more obvious to
|
||||
// callers, hopefully.
|
||||
}
|
||||
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/v3/download/";
|
||||
let prefix: string;
|
||||
if (useAuthentication) {
|
||||
// TODO: Use stable once available (requires FCP on MSC3916).
|
||||
prefix = "/_matrix/client/unstable/org.matrix.msc3916/media/download/";
|
||||
} else {
|
||||
prefix = "/_matrix/media/v3/download/";
|
||||
}
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (width) {
|
||||
@@ -68,7 +89,12 @@ export function getHttpUriForMxc(
|
||||
if (Object.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v3/thumbnail/";
|
||||
if (useAuthentication) {
|
||||
// TODO: Use stable once available (requires FCP on MSC3916).
|
||||
prefix = "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/";
|
||||
} else {
|
||||
prefix = "/_matrix/media/v3/thumbnail/";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof allowRedirects === "boolean") {
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SecretsBundle } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import { Room } from "./models/room";
|
||||
import { DeviceMap } from "./models/device";
|
||||
@@ -532,6 +533,23 @@ export interface CryptoApi {
|
||||
* to false.
|
||||
*/
|
||||
startDehydration(createNewKey?: boolean): Promise<void>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Import/export of secret keys
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Export secrets bundle for transmitting to another device as part of OIDC QR login
|
||||
*/
|
||||
exportSecretsBundle?(): Promise<Awaited<ReturnType<SecretsBundle["to_json"]>>>;
|
||||
|
||||
/**
|
||||
* Import secrets bundle transmitted from another device.
|
||||
* @param secrets - The secrets bundle received from the other device
|
||||
*/
|
||||
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;
|
||||
}
|
||||
|
||||
/** A reason code for a failure to decrypt an event. */
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface BackupTrustInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
* The result of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
export interface KeyBackupCheck {
|
||||
backupInfo: KeyBackupInfo;
|
||||
|
||||
@@ -24,13 +24,13 @@ import { logger } from "../logger";
|
||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
|
||||
import { decryptAES, encryptAES } from "./aes";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
import { ISignedKey, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
|
||||
import { DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
|
||||
import { CrossSigningKeyInfo, DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
|
||||
import { decodeBase64, encodeBase64 } from "../base64";
|
||||
|
||||
// backwards-compatibility re-exports
|
||||
@@ -38,7 +38,7 @@ export { UserTrustLevel };
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
|
||||
function publicKeyFromKeyInfo(keyInfo: CrossSigningKeyInfo): string {
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
@@ -51,13 +51,13 @@ export interface ICacheCallbacks {
|
||||
}
|
||||
|
||||
export interface ICrossSigningInfo {
|
||||
keys: Record<string, ICrossSigningKey>;
|
||||
keys: Record<string, CrossSigningKeyInfo>;
|
||||
firstUse: boolean;
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo {
|
||||
public keys: Record<string, ICrossSigningKey> = {};
|
||||
public keys: Record<string, CrossSigningKeyInfo> = {};
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
// When you verify a user, any devices online at the time that receive
|
||||
@@ -296,7 +296,7 @@ export class CrossSigningInfo {
|
||||
}
|
||||
|
||||
const privateKeys: Record<string, Uint8Array> = {};
|
||||
const keys: Record<string, ICrossSigningKey> = {};
|
||||
const keys: Record<string, CrossSigningKeyInfo> = {};
|
||||
let masterSigning: PkSigning | undefined;
|
||||
let masterPub: string | undefined;
|
||||
|
||||
@@ -368,8 +368,8 @@ export class CrossSigningInfo {
|
||||
this.keys = {};
|
||||
}
|
||||
|
||||
public setKeys(keys: Record<string, ICrossSigningKey>): void {
|
||||
const signingKeys: Record<string, ICrossSigningKey> = {};
|
||||
public setKeys(keys: Record<string, CrossSigningKeyInfo>): void {
|
||||
const signingKeys: Record<string, CrossSigningKeyInfo> = {};
|
||||
if (keys.master) {
|
||||
if (keys.master.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId;
|
||||
@@ -457,7 +457,7 @@ export class CrossSigningInfo {
|
||||
}
|
||||
}
|
||||
|
||||
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
|
||||
public async signUser(key: CrossSigningInfo): Promise<CrossSigningKeyInfo | undefined> {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
|
||||
@@ -20,22 +20,15 @@ import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { Method, ClientPrefix } from "../http-api";
|
||||
import { Crypto, ICryptoCallbacks } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../client";
|
||||
import { ClientEvent, ClientEventHandlerMap, CrossSigningKeys, ISignedKey, KeySignatures } from "../client";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage";
|
||||
import { BootstrapCrossSigningOpts } from "../crypto-api";
|
||||
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
|
||||
keys: Record<"master" | "self_signing" | "user_signing", CrossSigningKeyInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
-26
@@ -56,14 +56,7 @@ import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import { ToDeviceBatch } from "../models/ToDeviceMessage";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICrossSigningKey,
|
||||
IKeysUploadResponse,
|
||||
ISignedKey,
|
||||
IUploadKeySignaturesResponse,
|
||||
MatrixClient,
|
||||
} from "../client";
|
||||
import { ClientEvent, IKeysUploadResponse, ISignedKey, IUploadKeySignaturesResponse, MatrixClient } from "../client";
|
||||
import { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
@@ -89,6 +82,7 @@ import { ISecretRequest } from "./SecretSharing";
|
||||
import {
|
||||
BackupTrustInfo,
|
||||
BootstrapCrossSigningOpts,
|
||||
CrossSigningKeyInfo,
|
||||
CrossSigningStatus,
|
||||
DecryptionFailureCode,
|
||||
DeviceVerificationStatus,
|
||||
@@ -649,7 +643,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getVersion}.
|
||||
* Implementation of {@link Crypto.CryptoApi#getVersion}.
|
||||
*/
|
||||
public getVersion(): string {
|
||||
const olmVersionTuple = Crypto.getOlmVersion();
|
||||
@@ -809,7 +803,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||
* Implementation of {@link Crypto.CryptoApi#getCrossSigningStatus}
|
||||
*/
|
||||
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
||||
const publicKeysOnDevice = Boolean(this.crossSigningInfo.getId());
|
||||
@@ -1173,7 +1167,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#resetKeyBackup}.
|
||||
* Implementation of {@link Crypto.CryptoApi#resetKeyBackup}.
|
||||
*/
|
||||
public async resetKeyBackup(): Promise<void> {
|
||||
// Delete existing ones
|
||||
@@ -1209,7 +1203,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
|
||||
* Implementation of {@link Crypto.CryptoApi#deleteKeyBackupVersion}.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
await this.backupManager.deleteKeyBackupVersion(version);
|
||||
@@ -1356,7 +1350,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
* Implementation of {@link CryptoApi.getActiveSessionBackupVersion}.
|
||||
* Implementation of {@link Crypto.CryptoApi.getActiveSessionBackupVersion}.
|
||||
*/
|
||||
public async getActiveSessionBackupVersion(): Promise<string | null> {
|
||||
if (this.backupManager.getKeyBackupEnabled()) {
|
||||
@@ -1378,7 +1372,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* Implementation of {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
* Implementation of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
|
||||
const checkResult = await this.backupManager.checkKeyBackup();
|
||||
@@ -1527,7 +1521,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
private async checkForValidDeviceSignature(
|
||||
userId: string,
|
||||
key: ICrossSigningKey,
|
||||
key: CrossSigningKeyInfo,
|
||||
devices: Record<string, IDevice>,
|
||||
): Promise<string[]> {
|
||||
const deviceIds: string[] = [];
|
||||
@@ -1595,7 +1589,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.getUserVerificationStatus}.
|
||||
* Implementation of {@link Crypto.CryptoApi.getUserVerificationStatus}.
|
||||
*/
|
||||
public async getUserVerificationStatus(userId: string): Promise<UserTrustLevel> {
|
||||
return this.checkUserTrust(userId);
|
||||
@@ -1894,7 +1888,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
|
||||
*/
|
||||
public importBackedUpRoomKeys(keys: IMegolmSessionData[], opts: ImportRoomKeysOpts = {}): Promise<void> {
|
||||
public importBackedUpRoomKeys(
|
||||
keys: IMegolmSessionData[],
|
||||
backupVersion: string,
|
||||
opts: ImportRoomKeysOpts = {},
|
||||
): Promise<void> {
|
||||
opts.source = "backup";
|
||||
return this.importRoomKeys(keys, opts);
|
||||
}
|
||||
@@ -1904,7 +1902,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @param keys - The new trusted set of keys
|
||||
*/
|
||||
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
|
||||
private async storeTrustedSelfKeys(keys: Record<string, CrossSigningKeyInfo> | null): Promise<void> {
|
||||
if (keys) {
|
||||
this.crossSigningInfo.setKeys(keys);
|
||||
} else {
|
||||
@@ -1992,7 +1990,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @returns base64-encoded ed25519 key.
|
||||
*
|
||||
* @deprecated Use {@link CryptoApi#getOwnDeviceKeys}.
|
||||
* @deprecated Use {@link Crypto.CryptoApi#getOwnDeviceKeys}.
|
||||
*/
|
||||
public getDeviceEd25519Key(): string | null {
|
||||
return this.olmDevice.deviceEd25519Key;
|
||||
@@ -2003,14 +2001,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @returns base64-encoded curve25519 key.
|
||||
*
|
||||
* @deprecated Use {@link CryptoApi#getOwnDeviceKeys}
|
||||
* @deprecated Use {@link Crypto.CryptoApi#getOwnDeviceKeys}
|
||||
*/
|
||||
public getDeviceCurve25519Key(): string | null {
|
||||
return this.olmDevice.deviceCurve25519Key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
|
||||
* Implementation of {@link Crypto.CryptoApi#getOwnDeviceKeys}.
|
||||
*/
|
||||
public async getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
|
||||
if (!this.olmDevice.deviceCurve25519Key) {
|
||||
@@ -2346,7 +2344,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Mark the given device as locally verified.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#setDeviceVerified}.
|
||||
* Implementation of {@link Crypto.CryptoApi#setDeviceVerified}.
|
||||
*/
|
||||
public async setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
|
||||
await this.setDeviceVerification(userId, deviceId, verified);
|
||||
@@ -2355,7 +2353,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Blindly cross-sign one of our other devices.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#crossSignDevice}.
|
||||
* Implementation of {@link Crypto.CryptoApi#crossSignDevice}.
|
||||
*/
|
||||
public async crossSignDevice(deviceId: string): Promise<void> {
|
||||
await this.setDeviceVerified(this.userId, deviceId, true);
|
||||
@@ -2390,7 +2388,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
blocked: boolean | null = null,
|
||||
known: boolean | null = null,
|
||||
keys?: Record<string, string>,
|
||||
): Promise<DeviceInfo | CrossSigningInfo | ICrossSigningKey | undefined> {
|
||||
): Promise<DeviceInfo | CrossSigningInfo | CrossSigningKeyInfo | undefined> {
|
||||
// Check if the 'device' is actually a cross signing key
|
||||
// The js-sdk's verification treats cross-signing keys as devices
|
||||
// and so uses this method to mark them verified.
|
||||
@@ -2791,7 +2789,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
|
||||
* Implementation of {@link Crypto.CryptoApi.getEncryptionInfoForEvent}.
|
||||
*/
|
||||
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
|
||||
const encryptionInfo = this.getEventEncryptionInfo(event);
|
||||
@@ -4274,7 +4272,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
|
||||
* Implementation of {@link Crypto.CryptoApi#isEncryptionEnabledInRoom}.
|
||||
*/
|
||||
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
|
||||
return this.isRoomEncrypted(roomId);
|
||||
|
||||
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { TrackingStatus } from "../DeviceList";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
@@ -27,6 +26,7 @@ import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { DehydrationManager } from "../dehydration";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
import { CrossSigningKeyInfo } from "../../crypto-api";
|
||||
|
||||
/**
|
||||
* Internal module. Definitions for storage for the crypto module
|
||||
@@ -100,13 +100,13 @@ export interface CryptoStore {
|
||||
// Olm Account
|
||||
getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
|
||||
storeAccount(txn: unknown, accountPickle: string): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void;
|
||||
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
func: (key: SecretStorePrivateKeys[K] | null) => void,
|
||||
type: K,
|
||||
): void;
|
||||
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
|
||||
storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void;
|
||||
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
|
||||
@@ -33,11 +33,11 @@ import {
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IndexedDBCryptoStore } from "./indexeddb-crypto-store";
|
||||
import { CrossSigningKeyInfo } from "../../crypto-api";
|
||||
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
@@ -418,7 +418,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("crossSigningKeys");
|
||||
@@ -447,7 +447,7 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, CrossSigningKeyInfo>): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ import {
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { CrossSigningKeyInfo } from "../../crypto-api";
|
||||
|
||||
/*
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
@@ -420,7 +420,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
*/
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
): void {
|
||||
this.backend!.getCrossSigningKeys(txn, func);
|
||||
}
|
||||
@@ -444,7 +444,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param keys - keys object as getCrossSigningKeys()
|
||||
*/
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, CrossSigningKeyInfo>): void {
|
||||
this.backend!.storeCrossSigningKeys(txn, keys);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ import {
|
||||
} from "./base";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { safeSet } from "../../utils";
|
||||
import { CrossSigningKeyInfo } from "../../crypto-api";
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -531,8 +531,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, CrossSigningKeyInfo>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
func(keys);
|
||||
}
|
||||
|
||||
@@ -545,7 +545,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func(key);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void {
|
||||
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
SESSION_BATCH_SIZE,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { CrossSigningKeyInfo } from "../../crypto-api";
|
||||
|
||||
function encodeSessionKey(senderCurve25519Key: string, sessionId: string): string {
|
||||
return encodeURIComponent(senderCurve25519Key) + "/" + encodeURIComponent(sessionId);
|
||||
@@ -56,7 +56,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
private migrationState: MigrationState = MigrationState.NOT_STARTED;
|
||||
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
|
||||
private account: string | null = null;
|
||||
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
|
||||
private crossSigningKeys: Record<string, CrossSigningKeyInfo> | null = null;
|
||||
private privateKeys: Partial<SecretStorePrivateKeys> = {};
|
||||
|
||||
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
|
||||
@@ -319,7 +319,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
this.account = accountPickle;
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
func(this.crossSigningKeys);
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
func(result || null);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void {
|
||||
this.crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export class ReciprocateQRCode extends Base {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.reciprocateQREvent = {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
cancel: (): void => reject(newUserCancelledError()),
|
||||
};
|
||||
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
|
||||
});
|
||||
|
||||
@@ -308,8 +308,8 @@ export class SAS extends Base {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
cancel: (): void => reject(newUserCancelledError()),
|
||||
mismatch: (): void => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
@@ -19,4 +19,7 @@ export enum Method {
|
||||
Put = "PUT",
|
||||
Post = "POST",
|
||||
Delete = "DELETE",
|
||||
Options = "OPTIONS",
|
||||
Head = "HEAD",
|
||||
Patch = "PATCH",
|
||||
}
|
||||
|
||||
+4
-10
@@ -121,10 +121,6 @@ interface ThreepidCreds {
|
||||
type EmailIdentityDict = {
|
||||
type: AuthType.Email;
|
||||
threepid_creds: ThreepidCreds;
|
||||
/**
|
||||
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
|
||||
*/
|
||||
threepidCreds?: ThreepidCreds;
|
||||
session: string;
|
||||
};
|
||||
|
||||
@@ -343,15 +339,13 @@ export class InteractiveAuth<T> {
|
||||
sid: this.emailSid,
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
const isUrl = this.matrixClient.getIdentityServerUrl();
|
||||
if (isUrl) {
|
||||
creds.id_server = new URL(isUrl).host;
|
||||
}
|
||||
authDict = {
|
||||
type: EMAIL_STAGE_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,20 +114,6 @@ export { LocationAssetType } from "./@types/location";
|
||||
*/
|
||||
export * as Crypto from "./crypto-api";
|
||||
|
||||
/**
|
||||
* Backwards compatibility re-export
|
||||
* @internal
|
||||
* @deprecated use {@link Crypto.CryptoApi}
|
||||
*/
|
||||
export type { CryptoApi } from "./crypto-api";
|
||||
|
||||
/**
|
||||
* Backwards compatibility re-export
|
||||
* @internal
|
||||
* @deprecated use {@link Crypto.DeviceVerificationStatus}
|
||||
*/
|
||||
export { DeviceVerificationStatus } from "./crypto-api";
|
||||
|
||||
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,7 @@ import { ISendEventResponse } from "../@types/requests";
|
||||
import { FileType } from "../http-api";
|
||||
import { KnownMembership } from "../@types/membership";
|
||||
import { RoomPowerLevelsEventContent, SpaceChildEventContent } from "../@types/state_events";
|
||||
import { EncryptedFile, FileContent } from "../@types/media";
|
||||
import type { EncryptedFile, FileContent } from "../@types/media";
|
||||
|
||||
/**
|
||||
* The recommended defaults for a tree space's power levels. Note that this
|
||||
|
||||
@@ -592,43 +592,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
*/
|
||||
public addLiveEvent(
|
||||
event: MatrixEvent,
|
||||
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions,
|
||||
): void;
|
||||
/**
|
||||
* @deprecated In favor of the overload with `IAddLiveEventOptions`
|
||||
*/
|
||||
public addLiveEvent(
|
||||
event: MatrixEvent,
|
||||
duplicateStrategy?: DuplicateStrategy,
|
||||
fromCache?: boolean,
|
||||
roomState?: RoomState,
|
||||
): void;
|
||||
public addLiveEvent(
|
||||
event: MatrixEvent,
|
||||
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
|
||||
fromCache = false,
|
||||
roomState?: RoomState,
|
||||
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {},
|
||||
): void {
|
||||
let duplicateStrategy = (duplicateStrategyOrOpts as DuplicateStrategy) || DuplicateStrategy.Ignore;
|
||||
let timelineWasEmpty: boolean | undefined;
|
||||
if (typeof duplicateStrategyOrOpts === "object") {
|
||||
({
|
||||
duplicateStrategy = DuplicateStrategy.Ignore,
|
||||
fromCache = false,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
} = duplicateStrategyOrOpts);
|
||||
} else if (duplicateStrategyOrOpts !== undefined) {
|
||||
// Deprecation warning
|
||||
// FIXME: Remove after 2023-06-01 (technical debt)
|
||||
logger.warn(
|
||||
"Overload deprecated: " +
|
||||
"`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " +
|
||||
"is deprecated in favor of the overload with " +
|
||||
"`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.filter) {
|
||||
const events = this.filter.filterRoomTimeline([event]);
|
||||
if (!events.length) {
|
||||
|
||||
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { IMarkerFoundOptions, RoomState } from "./room-state";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
import { MatrixEvent } from "./event";
|
||||
@@ -361,30 +360,10 @@ export class EventTimeline {
|
||||
* @param event - new event
|
||||
* @param options - addEvent options
|
||||
*/
|
||||
public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void;
|
||||
/**
|
||||
* @deprecated In favor of the overload with `IAddEventOptions`
|
||||
*/
|
||||
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void;
|
||||
public addEvent(
|
||||
event: MatrixEvent,
|
||||
toStartOfTimelineOrOpts: boolean | IAddEventOptions,
|
||||
roomState?: RoomState,
|
||||
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false },
|
||||
): void {
|
||||
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
|
||||
let timelineWasEmpty: boolean | undefined;
|
||||
if (typeof toStartOfTimelineOrOpts === "object") {
|
||||
({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
|
||||
} else if (toStartOfTimelineOrOpts !== undefined) {
|
||||
// Deprecation warning
|
||||
// FIXME: Remove after 2023-06-01 (technical debt)
|
||||
logger.warn(
|
||||
"Overload deprecated: " +
|
||||
"`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " +
|
||||
"is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`",
|
||||
);
|
||||
}
|
||||
|
||||
if (!roomState) {
|
||||
roomState = toStartOfTimeline ? this.startState : this.endState;
|
||||
}
|
||||
|
||||
+6
-24
@@ -77,7 +77,6 @@ export interface IUnsigned {
|
||||
"invite_room_state"?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
|
||||
[UNSIGNED_MEMBERSHIP_FIELD.name]?: Membership | string;
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
@@ -98,19 +97,6 @@ export interface IEvent {
|
||||
membership?: Membership;
|
||||
unsigned: IUnsigned;
|
||||
redacts?: string;
|
||||
|
||||
/**
|
||||
* @deprecated in favour of `sender`
|
||||
*/
|
||||
user_id?: string;
|
||||
/**
|
||||
* @deprecated in favour of `unsigned.prev_content`
|
||||
*/
|
||||
prev_content?: IContent;
|
||||
/**
|
||||
* @deprecated in favour of `origin_server_ts`
|
||||
*/
|
||||
age?: number;
|
||||
}
|
||||
|
||||
export interface IAggregatedRelation {
|
||||
@@ -495,7 +481,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns The user ID, e.g. `@alice:matrix.org`
|
||||
*/
|
||||
public getSender(): string | undefined {
|
||||
return this.event.sender || this.event.user_id; // v2 / v1
|
||||
return this.event.sender; // v2 / v1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -669,7 +655,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
*/
|
||||
public getPrevContent(): IContent {
|
||||
// v2 then v1 then default
|
||||
return this.getUnsigned().prev_content || this.event.prev_content || {};
|
||||
return this.getUnsigned().prev_content || {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -693,7 +679,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns The age of this event in milliseconds.
|
||||
*/
|
||||
public getAge(): number | undefined {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
return this.getUnsigned().age;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -730,13 +716,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns The user's room membership, or `undefined` if the server does
|
||||
* not report it.
|
||||
*/
|
||||
public getMembershipAtEvent(): Membership | string | undefined {
|
||||
public getMembershipAtEvent(): Optional<Membership | string> {
|
||||
const unsigned = this.getUnsigned();
|
||||
if (typeof unsigned[UNSIGNED_MEMBERSHIP_FIELD.name] === "string") {
|
||||
return unsigned[UNSIGNED_MEMBERSHIP_FIELD.name];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership | string>(unsigned);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1084,7 +1066,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* signing the public curve25519 key with the ed25519 key.
|
||||
*
|
||||
* In general, applications should not use this method directly, but should
|
||||
* instead use {@link CryptoApi#getEncryptionInfoForEvent}.
|
||||
* instead use {@link Crypto.CryptoApi#getEncryptionInfoForEvent}.
|
||||
*/
|
||||
public getClaimedEd25519Key(): string | null {
|
||||
return this.claimedEd25519Key;
|
||||
|
||||
@@ -20,12 +20,13 @@ import { isNumber, removeHiddenChars } from "../utils";
|
||||
import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "../@types/partials";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
|
||||
import { KnownMembership } from "../@types/membership";
|
||||
import { RoomJoinRulesEventContent } from "../@types/state_events";
|
||||
|
||||
export interface IMarkerFoundOptions {
|
||||
/** Whether the timeline was empty before the marker event arrived in the
|
||||
@@ -962,7 +963,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
*/
|
||||
public getJoinRule(): JoinRule {
|
||||
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
|
||||
const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {};
|
||||
const joinRuleContent: Partial<RoomJoinRulesEventContent> = joinRuleEvent?.getContent() ?? {};
|
||||
return joinRuleContent["join_rule"] || JoinRule.Invite;
|
||||
}
|
||||
|
||||
|
||||
+3
-96
@@ -405,12 +405,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
private threads = new Map<string, Thread>();
|
||||
|
||||
/**
|
||||
* @deprecated This value is unreliable. It may not contain the last thread.
|
||||
* Use {@link Room.getLastThread} instead.
|
||||
*/
|
||||
public lastThread?: Thread;
|
||||
|
||||
/**
|
||||
* A mapping of eventId to all visibility changes to apply
|
||||
* to the event, by chronological order, as per
|
||||
@@ -605,27 +599,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return createEvent.getContent()["room_version"] ?? "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this room needs to be upgraded to a new version
|
||||
* @returns What version the room should be upgraded to, or null if
|
||||
* the room does not require upgrading at this time.
|
||||
* @deprecated Use #getRecommendedVersion() instead
|
||||
*/
|
||||
public shouldUpgradeToVersion(): string | null {
|
||||
// TODO: Remove this function.
|
||||
// This makes assumptions about which versions are safe, and can easily
|
||||
// be wrong. Instead, people are encouraged to use getRecommendedVersion
|
||||
// which determines a safer value. This function doesn't use that function
|
||||
// because this is not async-capable, and to avoid breaking the contract
|
||||
// we're deprecating this.
|
||||
|
||||
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
|
||||
return KNOWN_SAFE_ROOM_VERSION;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the recommended room version for the room. This returns an
|
||||
* object with 3 properties: `version` as the new version the
|
||||
@@ -1403,32 +1376,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there are any devices in the room that are unverified
|
||||
*
|
||||
* Note: Callers should first check if crypto is enabled on this device. If it is
|
||||
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
|
||||
* error will be thrown.
|
||||
*
|
||||
* @returns the result
|
||||
*
|
||||
* @deprecated Not supported under rust crypto. Instead, call {@link Room.getEncryptionTargetMembers},
|
||||
* {@link CryptoApi.getUserDeviceInfo}, and {@link CryptoApi.getDeviceVerificationStatus}.
|
||||
*/
|
||||
public async hasUnverifiedDevices(): Promise<boolean> {
|
||||
if (!this.hasEncryptionStateEvent()) {
|
||||
return false;
|
||||
}
|
||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const devices = this.client.getStoredDevicesForUser(member.userId);
|
||||
if (devices.some((device) => device.isUnverified())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timeline sets for this room.
|
||||
* @returns array of timeline sets for this room
|
||||
@@ -2479,15 +2426,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// and pass the event through this.
|
||||
thread.addEvents(events, false);
|
||||
|
||||
const isNewer =
|
||||
this.lastThread?.rootEvent &&
|
||||
rootEvent?.localTimestamp &&
|
||||
this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp;
|
||||
|
||||
if (!this.lastThread || isNewer) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
// We need to update the thread root events, but the thread may not be ready yet.
|
||||
// If it isn't, it will fire ThreadEvent.Update when it is and we'll call updateThreadRootEvents then.
|
||||
if (this.threadsReady && thread.initialEventsFetched) {
|
||||
@@ -2917,39 +2855,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param addLiveEventOptions - addLiveEvent options
|
||||
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
|
||||
*/
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void>;
|
||||
/**
|
||||
* @deprecated In favor of the overload with `IAddLiveEventOptions`
|
||||
*/
|
||||
public async addLiveEvents(
|
||||
events: MatrixEvent[],
|
||||
duplicateStrategy?: DuplicateStrategy,
|
||||
fromCache?: boolean,
|
||||
): Promise<void>;
|
||||
public async addLiveEvents(
|
||||
events: MatrixEvent[],
|
||||
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
|
||||
fromCache = false,
|
||||
): Promise<void> {
|
||||
let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
|
||||
let timelineWasEmpty: boolean | undefined = false;
|
||||
if (typeof duplicateStrategyOrOpts === "object") {
|
||||
({
|
||||
duplicateStrategy,
|
||||
fromCache = false,
|
||||
/* roomState, (not used here) */
|
||||
timelineWasEmpty,
|
||||
} = duplicateStrategyOrOpts);
|
||||
} else if (duplicateStrategyOrOpts !== undefined) {
|
||||
// Deprecation warning
|
||||
// FIXME: Remove after 2023-06-01 (technical debt)
|
||||
logger.warn(
|
||||
"Overload deprecated: " +
|
||||
"`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " +
|
||||
"is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`",
|
||||
);
|
||||
}
|
||||
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
|
||||
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||
}
|
||||
@@ -3250,7 +3157,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
content: strippedEvent.content,
|
||||
event_id: "$fake" + Date.now(),
|
||||
room_id: this.roomId,
|
||||
user_id: this.myUserId, // technically a lie
|
||||
sender: this.myUserId, // technically a lie
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ interface OidcRegistrationRequestBody {
|
||||
application_type: "web" | "native";
|
||||
}
|
||||
|
||||
export const DEVICE_CODE_SCOPE = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
|
||||
/**
|
||||
* Attempts dynamic registration against the configured registration endpoint
|
||||
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
|
||||
|
||||
+3
-24
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||
import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { OidcError } from "./error";
|
||||
@@ -139,28 +139,7 @@ export function isValidatedIssuerMetadata(
|
||||
validateOIDCIssuerWellKnown(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard JWT claims.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
||||
*/
|
||||
interface JwtClaims {
|
||||
[claim: string]: unknown;
|
||||
/** The "iss" (issuer) claim identifies the principal that issued the JWT. */
|
||||
iss?: string;
|
||||
/** The "sub" (subject) claim identifies the principal that is the subject of the JWT. */
|
||||
sub?: string;
|
||||
/** The "aud" (audience) claim identifies the recipients that the JWT is intended for. */
|
||||
aud?: string | string[];
|
||||
/** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. */
|
||||
exp?: number;
|
||||
// unused claims excluded
|
||||
}
|
||||
interface IdTokenClaims extends JwtClaims {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
export const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
try {
|
||||
return jwtDecode<IdTokenClaims>(token);
|
||||
} catch (error) {
|
||||
@@ -276,7 +255,7 @@ export type BearerTokenResponse = {
|
||||
expires_in?: number;
|
||||
// from oidc-client-ts
|
||||
expires_at?: number;
|
||||
id_token?: string;
|
||||
id_token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,12 @@ limitations under the License.
|
||||
|
||||
import { UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
|
||||
import {
|
||||
RendezvousChannel,
|
||||
RendezvousFailureListener,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from ".";
|
||||
import { IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
|
||||
import { logger } from "../logger";
|
||||
@@ -59,6 +64,9 @@ const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc390
|
||||
* 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;
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
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 { QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { ClientRendezvousFailureReason, MSC4108FailureReason, RendezvousError, RendezvousFailureListener } from ".";
|
||||
import { MatrixClient } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel";
|
||||
import { MatrixError } from "../http-api";
|
||||
import { sleep } from "../utils";
|
||||
import { DEVICE_CODE_SCOPE, discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig } from "../oidc";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
|
||||
/**
|
||||
* Enum representing the payload types transmissible over [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* secure channels.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export enum PayloadType {
|
||||
Protocols = "m.login.protocols",
|
||||
Protocol = "m.login.protocol",
|
||||
Failure = "m.login.failure",
|
||||
Success = "m.login.success",
|
||||
Secrets = "m.login.secrets",
|
||||
ProtocolAccepted = "m.login.protocol_accepted",
|
||||
Declined = "m.login.declined",
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing the base payload format for [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* messages sent over the secure channel.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export interface MSC4108Payload {
|
||||
type: PayloadType;
|
||||
}
|
||||
|
||||
interface ProtocolsPayload extends MSC4108Payload {
|
||||
type: PayloadType.Protocols;
|
||||
protocols: string[];
|
||||
homeserver: string;
|
||||
}
|
||||
|
||||
interface ProtocolPayload extends MSC4108Payload {
|
||||
type: PayloadType.Protocol;
|
||||
protocol: Exclude<string, "device_authorization_grant">;
|
||||
device_id: string;
|
||||
}
|
||||
|
||||
interface DeviceAuthorizationGrantProtocolPayload extends ProtocolPayload {
|
||||
protocol: "device_authorization_grant";
|
||||
device_authorization_grant: {
|
||||
verification_uri: string;
|
||||
verification_uri_complete?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function isDeviceAuthorizationGrantProtocolPayload(
|
||||
payload: ProtocolPayload,
|
||||
): payload is DeviceAuthorizationGrantProtocolPayload {
|
||||
return payload.protocol === "device_authorization_grant";
|
||||
}
|
||||
|
||||
interface FailurePayload extends MSC4108Payload {
|
||||
type: PayloadType.Failure;
|
||||
reason: MSC4108FailureReason;
|
||||
homeserver?: string;
|
||||
}
|
||||
|
||||
interface DeclinedPayload extends MSC4108Payload {
|
||||
type: PayloadType.Declined;
|
||||
}
|
||||
|
||||
interface SuccessPayload extends MSC4108Payload {
|
||||
type: PayloadType.Success;
|
||||
}
|
||||
|
||||
interface AcceptedPayload extends MSC4108Payload {
|
||||
type: PayloadType.ProtocolAccepted;
|
||||
}
|
||||
|
||||
interface SecretsPayload extends MSC4108Payload, Awaited<ReturnType<NonNullable<CryptoApi["exportSecretsBundle"]>>> {
|
||||
type: PayloadType.Secrets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prototype of the unstable [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* sign in with QR + OIDC flow.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export class MSC4108SignInWithQR {
|
||||
private readonly ourIntent: QrCodeMode;
|
||||
private _code?: Uint8Array;
|
||||
private expectingNewDeviceId?: string;
|
||||
|
||||
/**
|
||||
* Returns the check code for the secure channel or undefined if not generated yet.
|
||||
*/
|
||||
public get checkCode(): string | undefined {
|
||||
return this.channel?.getCheckCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel - The secure channel used for communication
|
||||
* @param client - The Matrix client in used on the device already logged in
|
||||
* @param didScanCode - Whether this side of the channel scanned the QR code from the other party
|
||||
* @param onFailure - Callback for when the rendezvous fails
|
||||
*/
|
||||
public constructor(
|
||||
private readonly channel: MSC4108SecureChannel,
|
||||
private readonly didScanCode: boolean,
|
||||
private readonly client?: MatrixClient,
|
||||
public onFailure?: RendezvousFailureListener,
|
||||
) {
|
||||
this.ourIntent = client ? QrCodeMode.Reciprocate : QrCodeMode.Login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
|
||||
*/
|
||||
public get code(): Uint8Array | 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;
|
||||
}
|
||||
|
||||
if (this.ourIntent === QrCodeMode.Reciprocate && this.client) {
|
||||
this._code = await this.channel.generateCode(this.ourIntent, this.client.getHomeserverUrl());
|
||||
} else if (this.ourIntent === QrCodeMode.Login) {
|
||||
this._code = await this.channel.generateCode(this.ourIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
|
||||
*/
|
||||
public get isExistingDevice(): boolean {
|
||||
return this.ourIntent === QrCodeMode.Reciprocate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device is the new device logging in being reciprocated by the device on the other side of the channel.
|
||||
*/
|
||||
public get isNewDevice(): boolean {
|
||||
return !this.isExistingDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first step in the OIDC QR login process.
|
||||
* To be called after the QR code has been rendered or scanned.
|
||||
* The scanning device has to discover the homeserver details, if they scanned the code then they already have it.
|
||||
* If the new device is the one rendering the QR code then it has to wait be sent the homeserver details via the rendezvous channel.
|
||||
*/
|
||||
public async negotiateProtocols(): Promise<{ homeserverBaseUrl?: string }> {
|
||||
logger.info(`negotiateProtocols(isNewDevice=${this.isNewDevice} didScanCode=${this.didScanCode})`);
|
||||
await this.channel.connect();
|
||||
|
||||
if (this.didScanCode) {
|
||||
// Secure Channel step 6 completed, we trust the channel
|
||||
|
||||
if (this.isNewDevice) {
|
||||
// MSC4108-Flow: ExistingScanned - take homeserver from QR code which should already be set
|
||||
} else {
|
||||
// MSC4108-Flow: NewScanned -send protocols message
|
||||
let oidcClientConfig: OidcClientConfig | undefined;
|
||||
try {
|
||||
const { issuer } = await this.client!.getAuthIssuer();
|
||||
oidcClientConfig = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||
} catch (e) {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
|
||||
if (oidcClientConfig?.metadata.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||
await this.send<ProtocolsPayload>({
|
||||
type: PayloadType.Protocols,
|
||||
protocols: ["device_authorization_grant"],
|
||||
homeserver: this.client?.getHomeserverUrl() ?? "",
|
||||
});
|
||||
} else {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnsupportedProtocol,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Device code grant unsupported",
|
||||
MSC4108FailureReason.UnsupportedProtocol,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (this.isNewDevice) {
|
||||
// MSC4108-Flow: ExistingScanned - wait for protocols message
|
||||
logger.info("Waiting for protocols message");
|
||||
const payload = await this.receive<ProtocolsPayload>();
|
||||
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
|
||||
if (payload?.type !== PayloadType.Protocols) {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Unexpected message received",
|
||||
MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
);
|
||||
}
|
||||
|
||||
return { homeserverBaseUrl: payload.homeserver };
|
||||
} else {
|
||||
// MSC4108-Flow: NewScanned - nothing to do
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The second & third step in the OIDC QR login process.
|
||||
* To be called after `negotiateProtocols` for the existing device.
|
||||
* To be called after OIDC negotiation for the new device. (Currently unsupported)
|
||||
*/
|
||||
public async deviceAuthorizationGrant(): Promise<{
|
||||
verificationUri?: string;
|
||||
userCode?: string;
|
||||
}> {
|
||||
if (this.isNewDevice) {
|
||||
throw new Error("New device flows around OIDC are not yet implemented");
|
||||
} else {
|
||||
// The user needs to do step 7 for the out-of-band confirmation
|
||||
// but, first we receive the protocol chosen by the other device so that
|
||||
// the confirmation_uri is ready to go
|
||||
logger.info("Waiting for protocol message");
|
||||
const payload = await this.receive<ProtocolPayload | DeviceAuthorizationGrantProtocolPayload>();
|
||||
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
|
||||
if (payload?.type !== PayloadType.Protocol) {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Unexpected message received",
|
||||
MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDeviceAuthorizationGrantProtocolPayload(payload)) {
|
||||
const { device_authorization_grant: dag, device_id: expectingNewDeviceId } = payload;
|
||||
const { verification_uri: verificationUri, verification_uri_complete: verificationUriComplete } = dag;
|
||||
|
||||
let deviceAlreadyExists = true;
|
||||
try {
|
||||
await this.client?.getDevice(expectingNewDeviceId);
|
||||
} catch (err: MatrixError | unknown) {
|
||||
if (err instanceof MatrixError && err.httpStatus === 404) {
|
||||
deviceAlreadyExists = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceAlreadyExists) {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.DeviceAlreadyExists,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Specified device ID already exists",
|
||||
MSC4108FailureReason.DeviceAlreadyExists,
|
||||
);
|
||||
}
|
||||
|
||||
this.expectingNewDeviceId = expectingNewDeviceId;
|
||||
|
||||
return { verificationUri: verificationUriComplete ?? verificationUri };
|
||||
}
|
||||
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnsupportedProtocol,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Received a request for an unsupported protocol",
|
||||
MSC4108FailureReason.UnsupportedProtocol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The fifth (and final) step in the OIDC QR login process.
|
||||
* To be called after the new device has completed authentication.
|
||||
*/
|
||||
public async shareSecrets(): Promise<{ secrets?: Omit<SecretsPayload, "type"> }> {
|
||||
if (this.isNewDevice) {
|
||||
await this.send<SuccessPayload>({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
// then wait for secrets
|
||||
logger.info("Waiting for secrets message");
|
||||
const payload = await this.receive<SecretsPayload>();
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
|
||||
if (payload?.type !== PayloadType.Secrets) {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
});
|
||||
throw new RendezvousError(
|
||||
"Unexpected message received",
|
||||
MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
);
|
||||
}
|
||||
return { secrets: payload };
|
||||
// then done?
|
||||
} else {
|
||||
if (!this.expectingNewDeviceId) {
|
||||
throw new Error("No new device ID expected");
|
||||
}
|
||||
await this.send<AcceptedPayload>({
|
||||
type: PayloadType.ProtocolAccepted,
|
||||
});
|
||||
|
||||
logger.info("Waiting for outcome message");
|
||||
const payload = await this.receive<SuccessPayload | DeclinedPayload>();
|
||||
|
||||
if (payload?.type === PayloadType.Failure) {
|
||||
throw new RendezvousError("Failed", payload.reason);
|
||||
}
|
||||
|
||||
if (payload?.type === PayloadType.Declined) {
|
||||
throw new RendezvousError("User declined", ClientRendezvousFailureReason.UserDeclined);
|
||||
}
|
||||
|
||||
if (payload?.type !== PayloadType.Success) {
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
});
|
||||
throw new RendezvousError("Unexpected message", MSC4108FailureReason.UnexpectedMessageReceived);
|
||||
}
|
||||
|
||||
const timeout = Date.now() + 10000; // wait up to 10 seconds
|
||||
do {
|
||||
// is the device visible via the Homeserver?
|
||||
try {
|
||||
const device = await this.client?.getDevice(this.expectingNewDeviceId);
|
||||
|
||||
if (device) {
|
||||
// if so, return the secrets
|
||||
const secretsBundle = await this.client!.getCrypto()!.exportSecretsBundle!();
|
||||
if (this.channel.cancelled) {
|
||||
throw new RendezvousError("User cancelled", MSC4108FailureReason.UserCancelled);
|
||||
}
|
||||
// send secrets
|
||||
await this.send<SecretsPayload>({
|
||||
type: PayloadType.Secrets,
|
||||
...secretsBundle,
|
||||
});
|
||||
return { secrets: secretsBundle };
|
||||
// let the other side close the rendezvous session
|
||||
}
|
||||
} catch (err: MatrixError | unknown) {
|
||||
if (err instanceof MatrixError && err.httpStatus === 404) {
|
||||
// not found, so keep waiting until timeout
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await sleep(1000);
|
||||
} while (Date.now() < timeout);
|
||||
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.DeviceNotFound,
|
||||
});
|
||||
throw new RendezvousError("New device not found", MSC4108FailureReason.DeviceNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
private async receive<T extends MSC4108Payload>(): Promise<T | FailurePayload | undefined> {
|
||||
return (await this.channel.secureReceive()) as T | undefined;
|
||||
}
|
||||
|
||||
private async send<T extends MSC4108Payload>(payload: T): Promise<void> {
|
||||
await this.channel.secureSend(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline the login on the existing device.
|
||||
*/
|
||||
public async declineLoginOnExistingDevice(): Promise<void> {
|
||||
if (!this.isExistingDevice) {
|
||||
throw new Error("Can only decline login on existing device");
|
||||
}
|
||||
await this.send<FailurePayload>({
|
||||
type: PayloadType.Failure,
|
||||
reason: MSC4108FailureReason.UserCancelled,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the rendezvous session.
|
||||
* @param reason the reason for the cancellation
|
||||
*/
|
||||
public async cancel(reason: MSC4108FailureReason | ClientRendezvousFailureReason): Promise<void> {
|
||||
this.onFailure?.(reason);
|
||||
await this.channel.cancel(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the rendezvous session.
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
await this.channel.close();
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,47 @@ limitations under the License.
|
||||
|
||||
export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void;
|
||||
|
||||
export enum RendezvousFailureReason {
|
||||
export type RendezvousFailureReason =
|
||||
| LegacyRendezvousFailureReason
|
||||
| MSC4108FailureReason
|
||||
| ClientRendezvousFailureReason;
|
||||
|
||||
export enum LegacyRendezvousFailureReason {
|
||||
UserDeclined = "user_declined",
|
||||
OtherDeviceNotSignedIn = "other_device_not_signed_in",
|
||||
OtherDeviceAlreadySignedIn = "other_device_already_signed_in",
|
||||
Unknown = "unknown",
|
||||
Expired = "expired",
|
||||
UserCancelled = "user_cancelled",
|
||||
InvalidCode = "invalid_code",
|
||||
UnsupportedAlgorithm = "unsupported_algorithm",
|
||||
DataMismatch = "data_mismatch",
|
||||
UnsupportedTransport = "unsupported_transport",
|
||||
UnsupportedProtocol = "unsupported_protocol",
|
||||
HomeserverLacksSupport = "homeserver_lacks_support",
|
||||
}
|
||||
|
||||
export enum MSC4108FailureReason {
|
||||
AuthorizationExpired = "authorization_expired",
|
||||
DeviceAlreadyExists = "device_already_exists",
|
||||
DeviceNotFound = "device_not_found",
|
||||
UnexpectedMessageReceived = "unexpected_message_received",
|
||||
UnsupportedProtocol = "unsupported_protocol",
|
||||
UserCancelled = "user_cancelled",
|
||||
}
|
||||
|
||||
export enum ClientRendezvousFailureReason {
|
||||
/** The sign in request has expired */
|
||||
Expired = "expired",
|
||||
/** The homeserver is lacking support for the required features */
|
||||
HomeserverLacksSupport = "homeserver_lacks_support",
|
||||
/** The secure channel verification failed meaning that it might be compromised */
|
||||
InsecureChannelDetected = "insecure_channel_detected",
|
||||
/** An invalid/incompatible QR code was scanned */
|
||||
InvalidCode = "invalid_code",
|
||||
/** The other device is not signed in */
|
||||
OtherDeviceNotSignedIn = "other_device_not_signed_in",
|
||||
/** The other device is already signed in */
|
||||
OtherDeviceAlreadySignedIn = "other_device_already_signed_in",
|
||||
/** Other */
|
||||
Unknown = "unknown",
|
||||
/** The user declined the sign in request */
|
||||
UserDeclined = "user_declined",
|
||||
/** The rendezvous request is missing an ETag header */
|
||||
ETagMissing = "etag_missing",
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
RendezvousChannel,
|
||||
RendezvousTransportDetails,
|
||||
RendezvousTransport,
|
||||
RendezvousFailureReason,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
} from "..";
|
||||
import { encodeUnpaddedBase64, decodeBase64 } from "../../base64";
|
||||
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
|
||||
@@ -69,6 +69,9 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||
* 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;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
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 {
|
||||
Curve25519PublicKey,
|
||||
Ecies,
|
||||
EstablishedEcies,
|
||||
QrCodeData,
|
||||
QrCodeMode,
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import {
|
||||
ClientRendezvousFailureReason,
|
||||
MSC4108FailureReason,
|
||||
MSC4108Payload,
|
||||
RendezvousError,
|
||||
RendezvousFailureListener,
|
||||
} from "..";
|
||||
import { MSC4108RendezvousSession } from "../transports/MSC4108RendezvousSession";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
/**
|
||||
* Prototype of the unstable [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* secure rendezvous session protocol.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
* Imports @matrix-org/matrix-sdk-crypto-wasm so should be async-imported to avoid bundling the WASM into the main bundle.
|
||||
*/
|
||||
export class MSC4108SecureChannel {
|
||||
private readonly secureChannel: Ecies;
|
||||
private establishedChannel?: EstablishedEcies;
|
||||
private connected = false;
|
||||
|
||||
public constructor(
|
||||
private rendezvousSession: MSC4108RendezvousSession,
|
||||
private theirPublicKey?: Curve25519PublicKey,
|
||||
public onFailure?: RendezvousFailureListener,
|
||||
) {
|
||||
this.secureChannel = new Ecies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a QR code for the current session.
|
||||
* @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`.
|
||||
* @param homeserverBaseUrl the base URL of the homeserver to connect to, required for `Reciprocate` mode.
|
||||
*/
|
||||
public async generateCode(mode: QrCodeMode.Login): Promise<Uint8Array>;
|
||||
public async generateCode(mode: QrCodeMode.Reciprocate, homeserverBaseUrl: string): Promise<Uint8Array>;
|
||||
public async generateCode(mode: QrCodeMode, homeserverBaseUrl?: string): Promise<Uint8Array> {
|
||||
const { url } = this.rendezvousSession;
|
||||
|
||||
if (!url) {
|
||||
throw new Error("No rendezvous session URL");
|
||||
}
|
||||
|
||||
return new QrCodeData(
|
||||
this.secureChannel.public_key(),
|
||||
url,
|
||||
mode === QrCodeMode.Reciprocate ? homeserverBaseUrl : undefined,
|
||||
).to_bytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the check code for the secure channel or undefined if not generated yet.
|
||||
*/
|
||||
public getCheckCode(): string | undefined {
|
||||
const x = this.establishedChannel?.check_code();
|
||||
|
||||
if (!x) {
|
||||
return undefined;
|
||||
}
|
||||
return Array.from(x.as_bytes())
|
||||
.map((b) => `${b % 10}`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects and establishes a secure channel with the other device.
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (this.connected) {
|
||||
throw new Error("Channel already connected");
|
||||
}
|
||||
|
||||
if (this.theirPublicKey) {
|
||||
// We are the scanning device
|
||||
const result = this.secureChannel.establish_outbound_channel(
|
||||
this.theirPublicKey,
|
||||
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||
);
|
||||
this.establishedChannel = result.channel;
|
||||
|
||||
/*
|
||||
Secure Channel step 4. Device S sends the initial message
|
||||
|
||||
Nonce := 0
|
||||
SH := ECDH(Ss, Gp)
|
||||
EncKey := HKDF_SHA256(SH, "MATRIX_QR_CODE_LOGIN|" || Gp || "|" || Sp, 0, 32)
|
||||
TaggedCiphertext := ChaCha20Poly1305_Encrypt(EncKey, Nonce, "MATRIX_QR_CODE_LOGIN_INITIATE")
|
||||
Nonce := Nonce + 2
|
||||
LoginInitiateMessage := UnpaddedBase64(TaggedCiphertext) || "|" || UnpaddedBase64(Sp)
|
||||
*/
|
||||
{
|
||||
logger.info("Sending LoginInitiateMessage");
|
||||
await this.rendezvousSession.send(result.initial_message);
|
||||
}
|
||||
|
||||
/*
|
||||
Secure Channel step 6. Verification by Device S
|
||||
|
||||
Nonce_G := 1
|
||||
(TaggedCiphertext, Sp) := Unpack(Message)
|
||||
Plaintext := ChaCha20Poly1305_Decrypt(EncKey, Nonce_G, TaggedCiphertext)
|
||||
Nonce_G := Nonce_G + 2
|
||||
|
||||
unless Plaintext == "MATRIX_QR_CODE_LOGIN_OK":
|
||||
FAIL
|
||||
*/
|
||||
{
|
||||
logger.info("Waiting for LoginOkMessage");
|
||||
const ciphertext = await this.rendezvousSession.receive();
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new RendezvousError(
|
||||
"No response from other device",
|
||||
MSC4108FailureReason.UnexpectedMessageReceived,
|
||||
);
|
||||
}
|
||||
const candidateLoginOkMessage = await this.decrypt(ciphertext);
|
||||
|
||||
if (candidateLoginOkMessage !== "MATRIX_QR_CODE_LOGIN_OK") {
|
||||
throw new RendezvousError(
|
||||
"Invalid response from other device",
|
||||
ClientRendezvousFailureReason.InsecureChannelDetected,
|
||||
);
|
||||
}
|
||||
|
||||
// Step 6 is now complete. We trust the channel
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
Secure Channel step 5. Device G confirms
|
||||
|
||||
Nonce_S := 0
|
||||
(TaggedCiphertext, Sp) := Unpack(LoginInitiateMessage)
|
||||
SH := ECDH(Gs, Sp)
|
||||
EncKey := HKDF_SHA256(SH, "MATRIX_QR_CODE_LOGIN|" || Gp || "|" || Sp, 0, 32)
|
||||
Plaintext := ChaCha20Poly1305_Decrypt(EncKey, Nonce_S, TaggedCiphertext)
|
||||
Nonce_S := Nonce_S + 2
|
||||
*/
|
||||
// wait for the other side to send us their public key
|
||||
logger.info("Waiting for LoginInitiateMessage");
|
||||
const loginInitiateMessage = await this.rendezvousSession.receive();
|
||||
if (!loginInitiateMessage) {
|
||||
throw new Error("No response from other device");
|
||||
}
|
||||
|
||||
const { channel, message: candidateLoginInitiateMessage } =
|
||||
this.secureChannel.establish_inbound_channel(loginInitiateMessage);
|
||||
this.establishedChannel = channel;
|
||||
|
||||
if (candidateLoginInitiateMessage !== "MATRIX_QR_CODE_LOGIN_INITIATE") {
|
||||
throw new RendezvousError(
|
||||
"Invalid response from other device",
|
||||
ClientRendezvousFailureReason.InsecureChannelDetected,
|
||||
);
|
||||
}
|
||||
logger.info("LoginInitiateMessage received");
|
||||
|
||||
logger.info("Sending LoginOkMessage");
|
||||
const loginOkMessage = await this.encrypt("MATRIX_QR_CODE_LOGIN_OK");
|
||||
await this.rendezvousSession.send(loginOkMessage);
|
||||
|
||||
// Step 5 is complete. We don't yet trust the channel
|
||||
|
||||
// next step will be for the user to confirm the check code on the other device
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
private async decrypt(ciphertext: string): Promise<string> {
|
||||
if (!this.establishedChannel) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
return this.establishedChannel.decrypt(ciphertext);
|
||||
}
|
||||
|
||||
private async encrypt(plaintext: string): Promise<string> {
|
||||
if (!this.establishedChannel) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
return this.establishedChannel.encrypt(plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a payload securely to the other device.
|
||||
* @param payload the payload to encrypt and send
|
||||
*/
|
||||
public async secureSend<T extends MSC4108Payload>(payload: T): Promise<void> {
|
||||
if (!this.connected) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
const stringifiedPayload = JSON.stringify(payload);
|
||||
logger.debug(`=> {"type": ${JSON.stringify(payload.type)}, ...}`);
|
||||
|
||||
await this.rendezvousSession.send(await this.encrypt(stringifiedPayload));
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an encrypted payload from the other device and decrypts it.
|
||||
*/
|
||||
public async secureReceive<T extends MSC4108Payload>(): Promise<Partial<T> | undefined> {
|
||||
if (!this.establishedChannel) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
const ciphertext = await this.rendezvousSession.receive();
|
||||
if (!ciphertext) {
|
||||
return undefined;
|
||||
}
|
||||
const plaintext = await this.decrypt(ciphertext);
|
||||
const json = JSON.parse(plaintext);
|
||||
|
||||
logger.debug(`<= {"type": ${JSON.stringify(json.type)}, ...}`);
|
||||
return json as Partial<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the secure channel.
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
await this.rendezvousSession.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the secure channel.
|
||||
* @param reason the reason for the cancellation
|
||||
*/
|
||||
public async cancel(reason: MSC4108FailureReason | ClientRendezvousFailureReason): Promise<void> {
|
||||
try {
|
||||
await this.rendezvousSession.cancel(reason);
|
||||
this.onFailure?.(reason);
|
||||
} finally {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the rendezvous session has been cancelled.
|
||||
*/
|
||||
public get cancelled(): boolean {
|
||||
return this.rendezvousSession.cancelled;
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3903ECDHv2RendezvousChannel";
|
||||
export * from "./MSC4108SecureChannel";
|
||||
|
||||
@@ -14,10 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3906Rendezvous";
|
||||
export * from "./MSC4108SignInWithQR";
|
||||
export * from "./RendezvousChannel";
|
||||
export * from "./RendezvousCode";
|
||||
export * from "./RendezvousError";
|
||||
export * from "./RendezvousFailureReason";
|
||||
export * from "./RendezvousIntent";
|
||||
export * from "./RendezvousTransport";
|
||||
export * from "./transports";
|
||||
export * from "./channels";
|
||||
|
||||
@@ -20,7 +20,7 @@ import { logger } from "../../logger";
|
||||
import { sleep } from "../../utils";
|
||||
import {
|
||||
RendezvousFailureListener,
|
||||
RendezvousFailureReason,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousTransport,
|
||||
RendezvousTransportDetails,
|
||||
} from "..";
|
||||
@@ -37,6 +37,9 @@ export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousT
|
||||
* 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;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
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 { logger } from "../../logger";
|
||||
import { sleep } from "../../utils";
|
||||
import { ClientRendezvousFailureReason, MSC4108FailureReason, RendezvousFailureListener } from "..";
|
||||
import { MatrixClient, Method } from "../../matrix";
|
||||
import { ClientPrefix } from "../../http-api";
|
||||
|
||||
/**
|
||||
* Prototype of the unstable [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
* insecure rendezvous session protocol.
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export class MSC4108RendezvousSession {
|
||||
public url?: string;
|
||||
private readonly client?: MatrixClient;
|
||||
private readonly fallbackRzServer?: string;
|
||||
private readonly fetchFn?: typeof global.fetch;
|
||||
private readonly onFailure?: RendezvousFailureListener;
|
||||
private etag?: string;
|
||||
private expiresAt?: Date;
|
||||
private expiresTimer?: ReturnType<typeof setTimeout>;
|
||||
private _cancelled = false;
|
||||
private _ready = false;
|
||||
|
||||
public constructor({
|
||||
onFailure,
|
||||
url,
|
||||
fetchFn,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
url: string;
|
||||
});
|
||||
public constructor({
|
||||
onFailure,
|
||||
client,
|
||||
fallbackRzServer,
|
||||
fetchFn,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
client?: MatrixClient;
|
||||
fallbackRzServer?: string;
|
||||
});
|
||||
public constructor({
|
||||
fetchFn,
|
||||
onFailure,
|
||||
url,
|
||||
client,
|
||||
fallbackRzServer,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
url?: string;
|
||||
client?: MatrixClient;
|
||||
fallbackRzServer?: string;
|
||||
}) {
|
||||
this.fetchFn = fetchFn;
|
||||
this.onFailure = onFailure;
|
||||
this.client = client;
|
||||
this.fallbackRzServer = fallbackRzServer;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the channel is ready to be used.
|
||||
*/
|
||||
public get ready(): boolean {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the channel has been cancelled.
|
||||
*/
|
||||
public get cancelled(): boolean {
|
||||
return this._cancelled;
|
||||
}
|
||||
|
||||
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> {
|
||||
if (this.client) {
|
||||
try {
|
||||
if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc4108")) {
|
||||
return this.client.http
|
||||
.getUrl("/org.matrix.msc4108/rendezvous", undefined, ClientPrefix.Unstable)
|
||||
.toString();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("Failed to get unstable features", err);
|
||||
}
|
||||
}
|
||||
|
||||
return this.fallbackRzServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data via the rendezvous channel.
|
||||
* @param data the payload to send
|
||||
*/
|
||||
public async send(data: string): Promise<void> {
|
||||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
const method = this.url ? Method.Put : Method.Post;
|
||||
const uri = this.url ?? (await this.getPostEndpoint());
|
||||
|
||||
if (!uri) {
|
||||
throw new Error("Invalid rendezvous URI");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "content-type": "text/plain" };
|
||||
|
||||
// if we didn't create the rendezvous channel, we need to fetch the first etag if needed
|
||||
if (!this.etag && this.url) {
|
||||
await this.receive();
|
||||
}
|
||||
|
||||
if (this.etag) {
|
||||
headers["if-match"] = this.etag;
|
||||
}
|
||||
|
||||
logger.info(`=> ${method} ${uri} with ${data} if-match: ${this.etag}`);
|
||||
|
||||
const res = await this.fetch(uri, { method, headers, body: data, redirect: "follow" });
|
||||
if (res.status === 404) {
|
||||
return this.cancel(ClientRendezvousFailureReason.Unknown);
|
||||
}
|
||||
this.etag = res.headers.get("etag") ?? undefined;
|
||||
|
||||
logger.info(`Received etag: ${this.etag}`);
|
||||
|
||||
if (method === Method.Post) {
|
||||
const expires = res.headers.get("expires");
|
||||
if (expires) {
|
||||
if (this.expiresTimer) {
|
||||
clearTimeout(this.expiresTimer);
|
||||
this.expiresTimer = undefined;
|
||||
}
|
||||
this.expiresAt = new Date(expires);
|
||||
this.expiresTimer = setTimeout(() => {
|
||||
this.expiresTimer = undefined;
|
||||
this.cancel(ClientRendezvousFailureReason.Expired);
|
||||
}, this.expiresAt.getTime() - Date.now());
|
||||
}
|
||||
// MSC4108: we expect a JSON response with a rendezvous URL
|
||||
const json = await res.json();
|
||||
if (typeof json.url !== "string") {
|
||||
throw new Error("No rendezvous URL given");
|
||||
}
|
||||
this.url = json.url;
|
||||
this._ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives data from the rendezvous channel.
|
||||
* @return the returned promise won't resolve until new data is acquired or the channel is closed either by the server or the other party.
|
||||
*/
|
||||
public async receive(): Promise<string | undefined> {
|
||||
if (!this.url) {
|
||||
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;
|
||||
}
|
||||
|
||||
logger.info(`=> GET ${this.url} if-none-match: ${this.etag}`);
|
||||
const poll = await this.fetch(this.url, { method: Method.Get, headers });
|
||||
|
||||
if (poll.status === 404) {
|
||||
await this.cancel(ClientRendezvousFailureReason.Unknown);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// rely on server expiring the channel rather than checking ourselves
|
||||
|
||||
const etag = poll.headers.get("etag") ?? undefined;
|
||||
if (poll.headers.get("content-type") !== "text/plain") {
|
||||
this.etag = etag;
|
||||
} else if (poll.status === 200) {
|
||||
if (!etag) {
|
||||
// Some browsers & extensions block the ETag header for anti-tracking purposes
|
||||
// We try and detect this so the client can give the user a somewhat helpful message
|
||||
await this.cancel(ClientRendezvousFailureReason.ETagMissing);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.etag = etag;
|
||||
const text = await poll.text();
|
||||
logger.info(`Received: ${text} with etag ${this.etag}`);
|
||||
return text;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the rendezvous channel.
|
||||
* If the reason is user_declined or user_cancelled then the channel will also be closed.
|
||||
* @param reason the reason to cancel with
|
||||
*/
|
||||
public async cancel(reason: MSC4108FailureReason | ClientRendezvousFailureReason): Promise<void> {
|
||||
if (this._cancelled) return;
|
||||
if (this.expiresTimer) {
|
||||
clearTimeout(this.expiresTimer);
|
||||
this.expiresTimer = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
reason === ClientRendezvousFailureReason.Unknown &&
|
||||
this.expiresAt &&
|
||||
this.expiresAt.getTime() < Date.now()
|
||||
) {
|
||||
reason = ClientRendezvousFailureReason.Expired;
|
||||
}
|
||||
|
||||
this._cancelled = true;
|
||||
this._ready = false;
|
||||
this.onFailure?.(reason);
|
||||
|
||||
if (reason === ClientRendezvousFailureReason.UserDeclined || reason === MSC4108FailureReason.UserCancelled) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the rendezvous channel.
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
if (this.expiresTimer) {
|
||||
clearTimeout(this.expiresTimer);
|
||||
this.expiresTimer = undefined;
|
||||
}
|
||||
|
||||
if (!this.url) return;
|
||||
try {
|
||||
await this.fetch(this.url, { method: Method.Delete });
|
||||
} catch (e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3886SimpleHttpRendezvousTransport";
|
||||
export * from "./MSC4108RendezvousSession";
|
||||
|
||||
@@ -57,10 +57,14 @@ class KeyDownloadRateLimitError extends Error {
|
||||
/** Details of a megolm session whose key we are trying to fetch. */
|
||||
type SessionInfo = { roomId: string; megolmSessionId: string };
|
||||
|
||||
/** Holds the current backup decryptor and version that should be used. */
|
||||
/** Holds the current backup decryptor and version that should be used.
|
||||
*
|
||||
* This is intended to be used as an immutable object (a new instance should be created if the configuration changes),
|
||||
* and some of the logic relies on that, so the properties are marked as `readonly`.
|
||||
*/
|
||||
type Configuration = {
|
||||
backupVersion: string;
|
||||
decryptor: BackupDecryptor;
|
||||
readonly backupVersion: string;
|
||||
readonly decryptor: BackupDecryptor;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -392,7 +396,7 @@ export class PerSessionKeyBackupDownloader {
|
||||
for (const k of keys) {
|
||||
k.room_id = sessionInfo.roomId;
|
||||
}
|
||||
await this.backupManager.importBackedUpRoomKeys(keys);
|
||||
await this.backupManager.importBackedUpRoomKeys(keys, configuration.backupVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -239,7 +239,11 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
|
||||
*/
|
||||
public async importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
|
||||
public async importBackedUpRoomKeys(
|
||||
keys: IMegolmSessionData[],
|
||||
backupVersion: string,
|
||||
opts?: ImportRoomKeysOpts,
|
||||
): Promise<void> {
|
||||
const keysByRoom: Map<RustSdkCryptoJs.RoomId, Map<string, IMegolmSessionData>> = new Map();
|
||||
for (const key of keys) {
|
||||
const roomId = new RustSdkCryptoJs.RoomId(key.room_id);
|
||||
@@ -259,6 +263,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
};
|
||||
opts?.progressCallback?.(importOpt);
|
||||
},
|
||||
backupVersion,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,13 +64,21 @@ export async function initRustCrypto(args: {
|
||||
storePrefix: string | null;
|
||||
|
||||
/**
|
||||
* A passphrase to use to encrypt the indexeddbs created by rust-crypto.
|
||||
* A passphrase to use to encrypt the indexeddb created by rust-crypto.
|
||||
*
|
||||
* Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs
|
||||
* will be unencrypted.
|
||||
* Ignored if `storePrefix` is null, or `storeKey` is set. If neither this nor `storeKey` is set
|
||||
* (and `storePrefix` is not null), the indexeddb will be unencrypted.
|
||||
*/
|
||||
storePassphrase?: string;
|
||||
|
||||
/**
|
||||
* A key to use to encrypt the indexeddb created by rust-crypto.
|
||||
*
|
||||
* Ignored if `storePrefix` is null. Otherwise, if it is set, it must be a 32-byte cryptographic key, which
|
||||
* will be used to encrypt the indexeddb. See also `storePassphrase`.
|
||||
*/
|
||||
storeKey?: Uint8Array;
|
||||
|
||||
/** If defined, we will check if any data needs migrating from this store to the rust store. */
|
||||
legacyCryptoStore?: CryptoStore;
|
||||
|
||||
@@ -94,10 +102,16 @@ export async function initRustCrypto(args: {
|
||||
new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn();
|
||||
|
||||
logger.debug("Opening Rust CryptoStore");
|
||||
const storeHandle: StoreHandle = await StoreHandle.open(
|
||||
args.storePrefix ?? undefined,
|
||||
(args.storePrefix && args.storePassphrase) ?? undefined,
|
||||
);
|
||||
let storeHandle;
|
||||
if (args.storePrefix) {
|
||||
if (args.storeKey) {
|
||||
storeHandle = await StoreHandle.openWithKey(args.storePrefix, args.storeKey);
|
||||
} else {
|
||||
storeHandle = await StoreHandle.open(args.storePrefix, args.storePassphrase);
|
||||
}
|
||||
} else {
|
||||
storeHandle = await StoreHandle.open();
|
||||
}
|
||||
|
||||
if (args.legacyCryptoStore) {
|
||||
// We have a legacy crypto store, which we may need to migrate from.
|
||||
|
||||
@@ -83,6 +83,19 @@ export async function migrateFromLegacyCrypto(args: {
|
||||
}
|
||||
|
||||
await legacyStore.startup();
|
||||
|
||||
let accountPickle: string | null = null;
|
||||
await legacyStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
legacyStore.getAccount(txn, (acctPickle) => {
|
||||
accountPickle = acctPickle;
|
||||
});
|
||||
});
|
||||
if (!accountPickle) {
|
||||
// This store is not properly set up. Nothing to migrate.
|
||||
logger.debug("Legacy crypto store is not set up (no account found). Not migrating.");
|
||||
return;
|
||||
}
|
||||
|
||||
let migrationState = await legacyStore.getMigrationState();
|
||||
|
||||
if (migrationState >= MigrationState.MEGOLM_SESSIONS_MIGRATED) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
CrossSigningKey,
|
||||
CrossSigningKeyInfo,
|
||||
CrossSigningStatus,
|
||||
CryptoApi,
|
||||
CryptoCallbacks,
|
||||
Curve25519AuthData,
|
||||
DecryptionFailureCode,
|
||||
@@ -308,6 +309,40 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") {
|
||||
throw new Error(`getBackupDecryptor Unsupported algorithm ${backupInfo.algorithm}`);
|
||||
}
|
||||
|
||||
const authData = <Curve25519AuthData>backupInfo.auth_data;
|
||||
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
throw new Error(`getBackupDecryptor expects Uint8Array`);
|
||||
}
|
||||
|
||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
|
||||
|
||||
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
|
||||
throw new Error(`getBackupDecryptor key mismatch error`);
|
||||
}
|
||||
|
||||
return this.backupManager.createBackupDecryptor(backupDecryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
|
||||
*/
|
||||
public async importBackedUpRoomKeys(
|
||||
keys: IMegolmSessionData[],
|
||||
backupVersion: string,
|
||||
opts?: ImportRoomKeysOpts,
|
||||
): Promise<void> {
|
||||
return await this.backupManager.importBackedUpRoomKeys(keys, backupVersion, opts);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CryptoApi implementation
|
||||
@@ -865,7 +900,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
|
||||
* Implementation of {@link Crypto.CryptoApi.getEncryptionInfoForEvent}.
|
||||
*/
|
||||
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
|
||||
return this.eventDecryptor.getEncryptionInfoForEvent(event);
|
||||
@@ -1169,6 +1204,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* Signs the given object with the current device and current identity (if available).
|
||||
* As defined in {@link https://spec.matrix.org/v1.8/appendices/#signing-json | Signing JSON}.
|
||||
*
|
||||
* Helper for {@link RustCrypto#resetKeyBackup}.
|
||||
*
|
||||
* @param obj - The object to sign
|
||||
*/
|
||||
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
|
||||
@@ -1192,44 +1229,14 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") {
|
||||
throw new Error(`getBackupDecryptor Unsupported algorithm ${backupInfo.algorithm}`);
|
||||
}
|
||||
|
||||
const authData = <Curve25519AuthData>backupInfo.auth_data;
|
||||
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
throw new Error(`getBackupDecryptor expects Uint8Array`);
|
||||
}
|
||||
|
||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
|
||||
|
||||
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
|
||||
throw new Error(`getBackupDecryptor key mismatch error`);
|
||||
}
|
||||
|
||||
return this.backupManager.createBackupDecryptor(backupDecryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
|
||||
*/
|
||||
public async importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
|
||||
return await this.backupManager.importBackedUpRoomKeys(keys, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#isDehydrationSupported}.
|
||||
* Implementation of {@link CryptoApi#isDehydrationSupported}.
|
||||
*/
|
||||
public async isDehydrationSupported(): Promise<boolean> {
|
||||
return await this.dehydratedDeviceManager.isSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#startDehydration}.
|
||||
* Implementation of {@link CryptoApi#startDehydration}.
|
||||
*/
|
||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
||||
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
|
||||
@@ -1238,6 +1245,26 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
return await this.dehydratedDeviceManager.start(createNewKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#importSecretsBundle}.
|
||||
*/
|
||||
public async importSecretsBundle(
|
||||
secrets: Parameters<NonNullable<CryptoApi["importSecretsBundle"]>>[0],
|
||||
): Promise<void> {
|
||||
const secretsBundle = RustSdkCryptoJs.SecretsBundle.from_json(secrets);
|
||||
await this.getOlmMachineOrThrow().importSecretsBundle(secretsBundle); // this method frees the SecretsBundle
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#exportSecretsBundle}.
|
||||
*/
|
||||
public async exportSecretsBundle(): ReturnType<NonNullable<CryptoApi["exportSecretsBundle"]>> {
|
||||
const secretsBundle = await this.getOlmMachineOrThrow().exportSecretsBundle();
|
||||
const secrets = secretsBundle.to_json();
|
||||
secretsBundle.free();
|
||||
return secrets;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
|
||||
@@ -567,8 +567,10 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
|
||||
// application to prompt the user to confirm their side.
|
||||
if (this.callbacks === null && this.inner.hasBeenScanned()) {
|
||||
this.callbacks = {
|
||||
confirm: () => this.confirmScanning(),
|
||||
cancel: () => this.cancel(),
|
||||
confirm: (): void => {
|
||||
this.confirmScanning();
|
||||
},
|
||||
cancel: (): void => this.cancel(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -708,10 +710,16 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
|
||||
}
|
||||
},
|
||||
mismatch: (): void => {
|
||||
throw new Error("impl");
|
||||
const request = this.inner.cancelWithCode("m.mismatched_sas");
|
||||
if (request) {
|
||||
this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
}
|
||||
},
|
||||
cancel: (): void => {
|
||||
throw new Error("impl");
|
||||
const request = this.inner.cancelWithCode("m.user");
|
||||
if (request) {
|
||||
this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
}
|
||||
},
|
||||
};
|
||||
this.emit(VerifierEvent.ShowSas, this.callbacks);
|
||||
|
||||
@@ -58,8 +58,6 @@ export interface IRoomEvent extends IMinimalEvent {
|
||||
event_id: string;
|
||||
sender: string;
|
||||
origin_server_ts: number;
|
||||
/** @deprecated - legacy field */
|
||||
age?: number;
|
||||
}
|
||||
|
||||
export interface IStateEvent extends IRoomEvent {
|
||||
@@ -498,7 +496,7 @@ export class SyncAccumulator {
|
||||
if (transformedEvent.unsigned !== undefined) {
|
||||
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
|
||||
}
|
||||
const age = e.unsigned ? e.unsigned.age : e.age;
|
||||
const age = e.unsigned?.age;
|
||||
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
|
||||
} else {
|
||||
transformedEvent = e;
|
||||
|
||||
+1
-6
@@ -748,12 +748,7 @@ export function safeSet<O extends Record<any, any>, K extends keyof O>(obj: O, p
|
||||
}
|
||||
|
||||
export function noUnsafeEventProps(event: Partial<IEvent>): boolean {
|
||||
return !(
|
||||
unsafeProp(event.room_id) ||
|
||||
unsafeProp(event.sender) ||
|
||||
unsafeProp(event.user_id) ||
|
||||
unsafeProp(event.event_id)
|
||||
);
|
||||
return !(unsafeProp(event.room_id) || unsafeProp(event.sender) || unsafeProp(event.event_id));
|
||||
}
|
||||
|
||||
export class MapWithDefault<K, V> extends Map<K, V> {
|
||||
|
||||
+30
-12
@@ -2381,22 +2381,40 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF before v113
|
||||
if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return;
|
||||
|
||||
const recvCodecs = RTCRtpReceiver.getCapabilities("video")!.codecs;
|
||||
const sendCodecs = RTCRtpSender.getCapabilities("video")!.codecs;
|
||||
const codecs = [...sendCodecs, ...recvCodecs];
|
||||
|
||||
for (const codec of codecs) {
|
||||
if (codec.mimeType === "video/rtx") {
|
||||
const rtxCodecIndex = codecs.indexOf(codec);
|
||||
codecs.splice(rtxCodecIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const screenshareVideoTransceiver = this.transceivers.get(
|
||||
getTransceiverKey(SDPStreamMetadataPurpose.Screenshare, "video"),
|
||||
);
|
||||
|
||||
// setCodecPreferences isn't supported on FF (as of v113)
|
||||
screenshareVideoTransceiver?.setCodecPreferences?.(codecs);
|
||||
if (!screenshareVideoTransceiver || !screenshareVideoTransceiver.setCodecPreferences) return;
|
||||
|
||||
const recvCodecs = RTCRtpReceiver.getCapabilities("video")!.codecs;
|
||||
const sendCodecs = RTCRtpSender.getCapabilities("video")!.codecs;
|
||||
const codecs = [];
|
||||
|
||||
for (const codec of [...recvCodecs, ...sendCodecs]) {
|
||||
if (codec.mimeType !== "video/rtx") {
|
||||
codecs.push(codec);
|
||||
try {
|
||||
screenshareVideoTransceiver.setCodecPreferences(codecs);
|
||||
} catch (e) {
|
||||
// Specifically, Chrome around version 125 and Electron 30 (which is Chromium 124) return an H.264 codec in
|
||||
// the sender's capabilities but throw when you try to set it. Hence... this mess.
|
||||
// Specifically, that codec is:
|
||||
// {
|
||||
// clockRate: 90000,
|
||||
// mimeType: "video/H264",
|
||||
// sdpFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640034",
|
||||
// }
|
||||
logger.info(
|
||||
"Working around buggy WebRTC impl: claimed to support codec but threw when setting codec preferences",
|
||||
codec,
|
||||
e,
|
||||
);
|
||||
codecs.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onNegotiationNeeded = async (): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user