Compare commits

...

49 Commits

Author SHA1 Message Date
ElementRobot 9e07710d80 v27.1.0 2023-08-01 12:27:34 +01:00
ElementRobot e9ed91d800 Prepare changelog for v27.1.0 2023-08-01 12:27:31 +01:00
ElementRobot 90da2cf439 v27.1.0-rc.1 2023-07-27 09:28:56 +01:00
ElementRobot 6edd45787b Prepare changelog for v27.1.0-rc.1 2023-07-27 09:28:52 +01:00
Florian Duros 84444ec11e ElementR: Add CryptoApi.getCrossSigningKeyId (#3619)
* Add `CryptoApi.getCrossSigningKeyId`

* Rename `CrossSigningPubKey` to `CrossSigningKeyInfo`

* Remove old eslint disable

* Review changes

* Review changes
2023-07-26 16:09:49 +00:00
Richard van der Hoff 0e95df5dba Element-R: implement {get,store}SessionBackupPrivateKey (#3622) 2023-07-26 12:00:43 +01:00
Valere 29b815b678 Replace deprecated TestClient with fetchMock (#3550)
* replace deprecated TestClient with fetchMock

* add stop() api to BackupManager for clean shutdown

* fix merge

* code review cleaning

* lint

* Address review comments

* Remove unused `TestClient.expectKeyBackupQuery`

* clean up imports

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-25 20:42:41 +00:00
Richard van der Hoff 0cf056958b Fix broken unit tests for FetchHttpApi.getUrl (#3620)
These tests have broken on Node.js 18.17.0.

This is due to Node.js adopting an updated version of the URL parser, in which
the internal `Symbol(query)` property is populated lazily.

We shouldn't be relying on the internal state of the URL object anyway. Let's
just compare the stringified copy.
2023-07-25 19:48:15 +00:00
Florian Duros 79d4113a6b ElementR: Stub CheckOwnCrossSigningTrust, import cross signing keys and verify local device in bootstrapCrossSigning (#3608) 2023-07-25 18:03:43 +01:00
Michael Telatynski 8a80886358 Fix threads ending up with chunks of their timelines missing (#3618)
* Fix threads ending up with chunks of their timelines missing

* delint
2023-07-25 15:28:52 +00:00
Michael Telatynski de7959de6c Ensure we do not clobber a newer RR with an older unthreaded one (#3617)
* Ensure we do not clobber a newer RR with an older unthreaded one

or vice versa

* Fix test
2023-07-24 16:35:56 +00:00
Michael Telatynski 533c21a515 Fix registration check your emails stage regression (#3616)
* Fix registration check your emails stage regression

* Simplify diff

* Add test
2023-07-24 14:08:17 +00:00
Michael Telatynski 6b018b6927 Fix how Room::eventShouldLiveIn handles replies to unknown parents (#3615)
* Add warning

* Fix how Room::eventShouldLiveIn handles replies to unknown parents
2023-07-24 07:37:28 +00:00
Michael Telatynski 38c3abb364 Update downstream-artifacts.yml (#3613) 2023-07-20 21:43:49 +00:00
Michael Telatynski a47f319665 Only send threaded read receipts if threads support is enabled (#3612)
* Only send threaded read receipts if threads support is enabled

* Tests
2023-07-20 15:44:52 +00:00
Florian Duros ecef9fd755 Fix CryptoApi#getVerificationRequestsToDeviceInProgress (#3611) 2023-07-20 09:46:55 +00:00
Richard van der Hoff 7dffd8ffd3 Make sure to drop references to user device lists (#3610)
Empirically, this seems to fix some problems with leaking references to
IndexedDB.
2023-07-20 08:47:30 +00:00
Michael Telatynski 66492e7ba8 Fix edge cases around non-thread relations to thread roots and read receipts (#3607)
* Ensure non-thread relations to a thread root are actually in both timelines

* Make thread in sendReceipt & sendReadReceipt explicit rather than guessing it

* Apply suggestions from code review

* Fix Room::eventShouldLiveIn to better match Synapse to diverging ideas of notifications

* Update read receipt sending behaviour to align with Synapse

* Fix tests

* Fix thread rel type
2023-07-19 11:21:50 +00:00
Michael Telatynski 43b2404865 Specify /preview_url requests as low priority (#3609)
* Specify /preview_url requests as low priority

* Update src/@types/global.d.ts

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

* Simplify interface

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-19 09:29:41 +00:00
Michael Telatynski fed9910fa1 Remove unused better-docs & docdash (#3605) 2023-07-19 07:44:11 +00:00
RiotRobot f77662406c Resetting package fields for development 2023-07-18 12:53:43 +01:00
RiotRobot 8cc0cf1a70 Merge branch 'master' into develop 2023-07-18 12:53:38 +01:00
RiotRobot dfa2429094 v27.0.0 2023-07-18 12:52:00 +01:00
RiotRobot 3e2460707c Prepare changelog for v27.0.0 2023-07-18 12:51:57 +01:00
Michael Telatynski 706c084fa7 Add tests for room-hierarchy (#3606)
* Add tests for room-hierarchy

* overwriteroutes
2023-07-18 10:37:17 +00:00
renovate[bot] eb7faa6c07 Lock file maintenance (#3604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-17 17:59:37 +00:00
Richard van der Hoff 0b193f4665 matrix-sdk-crypto-js -> matrix-sdk-crypto-wasm (#3602)
We've renamed matrix-sdk-crypto-js and released a v1.0.
2023-07-13 17:11:57 +00:00
Eric Eastwood 8ef2e848b9 Log query parameters on HTTP requests (#3591)
* Log query parameters on HTTP requests

Follow-up to https://github.com/matrix-org/matrix-js-sdk/pull/3485

* Only stringify once

See https://github.com/matrix-org/matrix-js-sdk/pull/3591#discussion_r1261300323
2023-07-13 13:07:01 +00:00
Richard van der Hoff d92936fba5 Element-R: support for displaying QR codes during verification (#3588)
* Support for showing QR codes

* Emit `VerificationRequestEvent.Change` events when the verifier changes

* Minor integ test tweaks

* Handle transitions from QR code display to SAS

* Fix naming

* Add a test for `ShowQrCodeCallbacks.cancel`
2023-07-13 11:11:13 +00:00
Michael Telatynski f005984df3 Export typed event emitter key types (#3597)
* Export typed event emitter key types

* Update src/matrix.ts
2023-07-13 11:10:24 +00:00
Richard van der Hoff 13fec49e74 Element-R: ensure that userHasCrossSigningKeys uses up-to-date data (#3599)
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data

* Bump matrix-sdk-crypto-js
2023-07-13 10:46:56 +00:00
renovate[bot] 008294cfc6 Update babel monorepo to v7.22.9 (#3434)
* Update babel monorepo to v7.22.9

* Make babel happier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-07-13 09:41:03 +00:00
Michael Telatynski b05f933d83 Fix read receipt sending behaviour around thread roots (#3600)
* Fix read receipt sending behaviour around thread roots

* Update src/client.ts

Co-authored-by: Eric Eastwood <erice@element.io>

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2023-07-13 08:25:23 +00:00
Michael Telatynski b186d79dde Fix jest/valid-expects lints (#3586) 2023-07-12 17:11:52 +00:00
Michael Telatynski e82b5fe1db Fix types in getSessionBackupPrivateKey (#3595)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment

* Fix types

* Fix types for MatrixClient::addThreePid

* Iterate
2023-07-12 14:38:14 +00:00
Michael Telatynski 9602aa88ea Fix sending auth: null due to broken types around UIA (#3594)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment
2023-07-12 13:55:02 +00:00
renovate[bot] 0fb3dc1b13 Update typescript-eslint monorepo to v5.62.0 (#3583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 11:01:00 +00:00
renovate[bot] aeede332be Update dependency @types/jest to v29.5.3 (#3582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:59:05 +00:00
renovate[bot] b052950a19 Update JS-DevTools/npm-publish action to v2.2.1 (#3581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:57:45 +00:00
Michael Telatynski 1cb5fff5a1 Improve types (#3589)
* Improve types

* Improve coverage
2023-07-12 10:39:33 +00:00
Michael Telatynski 01226e41d9 Fix broken DeviceList.spec.ts test (#3590) 2023-07-12 10:01:57 +00:00
dependabot[bot] e3919fd93b Bump semver from 5.7.1 to 5.7.2 (#3575)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-07-12 02:20:47 +00:00
Kerry 3b88ea19b7 Stabilize support for MSC3952: intentional mentions (#3397)
* use stable identifiers for MSC3952: intentional mentions

* add matrix version to feature support for intentional mentions
2023-07-11 22:04:06 +00:00
Richard van der Hoff dcf26f3e48 bump rust-sdk (#3587) 2023-07-11 17:40:39 +00:00
Valere 3385adf5f6 Improve logging of http requests to aid debugging (#3485)
* Simple request logging with status and duration

* remove url params from logs

* superfluous toString()

* Add tests

* Apply suggestions from code review

* update snapshots

* update log format

* Apply suggestions from code review

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* update snapshot

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-07-11 17:27:42 +00:00
Richard van der Hoff 9db6ce107a Add support for scanning QR codes during verification, with Rust crypto (#3565)
* Offer `m.qr_code.scan.v1` verification method by default

Normally, the application specifies the supported verification methods when
creating the MatrixClient (and matrix-react-sdk does so). If the application
leaves it unset, then the idea is that the js-sdk offers all known verification
methods.

However, by default, the rust-sdk doesn't specify `m.qr_code.scan.v1`. So
basically, we need to set our own list of supported methods, rather than
relying on the rust-sdk's defaults.

* Factor out base class from `RustSASVerifier`

* Implement QR code scanning

* Update src/rust-crypto/verification.ts
2023-07-11 16:00:59 +00:00
Florian Duros d5b22e1deb Use cryptoBackend in client.ts for new rust-crypto implementation (#3576)
* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility

* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility
2023-07-11 14:13:53 +00:00
Richard van der Hoff a5e606a1e7 Mark all the rust crypto stuff internal (#3574)
... for the avoidance of doubt.
2023-07-11 14:11:35 +00:00
Michael Telatynski f2471b6dbd Add methods to influence set_presence on /sync API calls (#3578)
* Add methods to influence set_presence on /sync API calls

* Tweak comment

* Improve coverage
2023-07-11 13:31:12 +00:00
74 changed files with 2535 additions and 1650 deletions
-3
View File
@@ -66,9 +66,6 @@ module.exports = {
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// TODO: There are many tests with invalid expects that should be fixed,
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
"jest/valid-expect": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
+3 -1
View File
@@ -1,6 +1,8 @@
name: Build downstream artifacts
on:
pull_request: {}
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
# Anyone working on Rust Crypto is able to run the tests locally if required.
merge_group:
types: [checks_requested]
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@a25b4180b728b0279fca97d4e5bccf391685aead # v2.2.0
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+27 -5
View File
@@ -1,20 +1,41 @@
Changes in [27.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0-rc.2) (2023-07-14)
============================================================================================================
Changes in [27.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0) (2023-08-01)
==================================================================================================
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* ElementR: Add `CryptoApi.getCrossSigningKeyId` ([\#3619](https://github.com/matrix-org/matrix-js-sdk/pull/3619)). Contributed by @florianduros.
* ElementR: Stub `CheckOwnCrossSigningTrust`, import cross signing keys and verify local device in `bootstrapCrossSigning` ([\#3608](https://github.com/matrix-org/matrix-js-sdk/pull/3608)). Contributed by @florianduros.
* Specify /preview_url requests as low priority ([\#3609](https://github.com/matrix-org/matrix-js-sdk/pull/3609)). Fixes vector-im/element-web#7292.
* Element-R: support for displaying QR codes during verification ([\#3588](https://github.com/matrix-org/matrix-js-sdk/pull/3588)). Fixes vector-im/crypto-internal#124.
* Add support for scanning QR codes during verification, with Rust crypto ([\#3565](https://github.com/matrix-org/matrix-js-sdk/pull/3565)).
* Add methods to influence set_presence on /sync API calls ([\#3578](https://github.com/matrix-org/matrix-js-sdk/pull/3578)).
## 🐛 Bug Fixes
* Fix threads ending up with chunks of their timelines missing ([\#3618](https://github.com/matrix-org/matrix-js-sdk/pull/3618)). Fixes vector-im/element-web#24466.
* Ensure we do not clobber a newer RR with an older unthreaded one ([\#3617](https://github.com/matrix-org/matrix-js-sdk/pull/3617)). Fixes vector-im/element-web#25806.
* Fix registration check your emails stage regression ([\#3616](https://github.com/matrix-org/matrix-js-sdk/pull/3616)).
* Fix how `Room::eventShouldLiveIn` handles replies to unknown parents ([\#3615](https://github.com/matrix-org/matrix-js-sdk/pull/3615)). Fixes vector-im/element-web#22603.
* Only send threaded read receipts if threads support is enabled ([\#3612](https://github.com/matrix-org/matrix-js-sdk/pull/3612)).
* ElementR: Fix `userId` parameter usage in `CryptoApi#getVerificationRequestsToDeviceInProgress` ([\#3611](https://github.com/matrix-org/matrix-js-sdk/pull/3611)). Contributed by @florianduros.
* Fix edge cases around non-thread relations to thread roots and read receipts ([\#3607](https://github.com/matrix-org/matrix-js-sdk/pull/3607)).
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Export typed event emitter key types ([\#3597](https://github.com/matrix-org/matrix-js-sdk/pull/3597)). Fixes #3506.
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data ([\#3599](https://github.com/matrix-org/matrix-js-sdk/pull/3599)). Fixes vector-im/element-web#25773.
* Fix sending `auth: null` due to broken types around UIA ([\#3594](https://github.com/matrix-org/matrix-js-sdk/pull/3594)).
Changes in [27.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0-rc.1) (2023-07-11)
============================================================================================================
Changes in [27.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0) (2023-07-18)
==================================================================================================
## 🚨 BREAKING CHANGES
* Drop support for Node 16 ([\#3533](https://github.com/matrix-org/matrix-js-sdk/pull/3533)).
* Improve types around login, registration, UIA and identity servers ([\#3537](https://github.com/matrix-org/matrix-js-sdk/pull/3537)).
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
* Simplify `MatrixClient::setPowerLevel` API ([\#3570](https://github.com/matrix-org/matrix-js-sdk/pull/3570)). Fixes vector-im/element-web#13900 and #1844.
* Deprecate `VerificationRequest.getQRCodeBytes` and replace it with the asynchronous `generateQRCode`. ([\#3562](https://github.com/matrix-org/matrix-js-sdk/pull/3562)).
* Drop support for Node 16 ([\#3533](https://github.com/matrix-org/matrix-js-sdk/pull/3533)).
* Deprecate `VerificationRequest.beginKeyVerification()` in favour of `VerificationRequest.startVerification()`. ([\#3528](https://github.com/matrix-org/matrix-js-sdk/pull/3528)).
* Deprecate `Crypto.VerificationRequest` application event, replacing it with `Crypto.VerificationRequestReceived`. ([\#3514](https://github.com/matrix-org/matrix-js-sdk/pull/3514)).
@@ -29,6 +50,7 @@ Changes in [27.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
* OIDC: validate id token ([\#3531](https://github.com/matrix-org/matrix-js-sdk/pull/3531)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Fix `TypedEventEmitter::removeAllListeners(void)` not working ([\#3561](https://github.com/matrix-org/matrix-js-sdk/pull/3561)).
* Don't allow Olm unwedging rate-limiting to race ([\#3549](https://github.com/matrix-org/matrix-js-sdk/pull/3549)). Fixes vector-im/element-web#25716.
* Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. ([\#3486](https://github.com/matrix-org/matrix-js-sdk/pull/3486)).
+2 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "27.0.0-rc.2",
"version": "27.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -55,7 +55,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.1",
"@matrix-org/matrix-sdk-crypto-wasm": "^1.1.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@@ -97,11 +97,9 @@
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.44.0",
"eslint-config-google": "^0.14.0",
-17
View File
@@ -32,8 +32,6 @@ import { syncPromise } from "./test-utils/test-utils";
import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
import { ISyncResponder } from "./test-utils/SyncResponder";
@@ -214,21 +212,6 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
});
}
/**
* Set up expectations that the client will query key backups for a particular session
*/
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
this.httpBackend
.when(
"GET",
encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
}),
)
.respond(status, response);
}
/**
* get the uploaded curve25519 device key
*
+186 -4
View File
@@ -18,9 +18,22 @@ import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { createClient, IAuthDict, MatrixClient } from "../../../src";
import { mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
} from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -39,8 +52,32 @@ const TEST_DEVICE_ID = "xzcvb";
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
// Encryption key used to encrypt cross signing keys
const encryptionKey = new Uint8Array(32);
/**
* Create the {@link CryptoCallbacks}
*/
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
},
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
@@ -52,8 +89,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
await initCrypto(aliceClient);
});
@@ -68,7 +111,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
await aliceClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
});
@@ -105,6 +148,94 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
// Return public cross signing keys
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
);
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: masterKey,
},
},
},
{
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: selfSigningKey,
},
},
},
{
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: userSigningKey,
},
},
},
{
type: "m.secret_storage.key.key_id",
content: {
key: "key_id",
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
);
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Check if the UserTrustStatusChanged event was fired
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
});
});
describe("getCrossSigningStatus()", () => {
@@ -157,4 +288,55 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
expect(isCrossSigningReady).toBeTruthy();
});
});
describe("getCrossSigningKeyId", () => {
/**
* Intercept /keys/device_signing/upload request and return the cross signing keys
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
*
* @returns the cross signing keys
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
resolve(content);
return {};
},
// Override the routes define in `mockSetupCrossSigningRequests`
{ overwriteRoutes: true },
);
});
}
it("should return the cross signing key id for each cross signing key", async () => {
mockSetupCrossSigningRequests();
// Intercept cross signing keys upload
const crossSigningKeysPromise = awaitCrossSigningKeysUpload();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Get the cross signing keys
const crossSigningKeys = await crossSigningKeysPromise;
const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0];
const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId();
expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key));
const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning);
expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key));
const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning);
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
});
});
});
+89 -75
View File
@@ -14,17 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Account } from "@matrix-org/olm";
import fetchMock from "fetch-mock-jest";
import { logger } from "../../../src/logger";
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
import { TestClient } from "../../TestClient";
import { IEvent } from "../../../src";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
import { MatrixEventEvent } from "../../../src/models/event";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { syncPromise } from "../../test-utils/test-utils";
const ROOM_ID = "!ROOM:ID";
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
const TEST_HOMESERVER_URL = "https://alice-server.com";
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
const ENCRYPTED_EVENT: Partial<IEvent> = {
@@ -70,57 +77,65 @@ const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
// Will be updated with correct value on the fly
signatures: {},
},
};
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/**
* start an Olm session with a given recipient
*/
function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
});
}
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
describe("megolm key backups", function () {
if (!global.Olm) {
logger.warn("not running megolm tests: Olm not present");
return;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;
/** an object which intercepts `/keys/upload` requests on the test homeserver */
let e2eKeyReceiver: E2EKeyReceiver;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
jest.useFakeTimers();
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
...opts,
});
await client.initCrypto();
return client;
}
const Olm = global.Olm;
let testOlmAccount: Olm.Account;
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs");
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function () {
return Olm.init();
});
beforeEach(async function () {
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function () {
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function () {
it("Alice checks key backups when receiving a message she can't decrypt", async function () {
const syncResponse = {
next_batch: 1,
rooms: {
@@ -134,37 +149,36 @@ describe("megolm key backups", function () {
},
};
return aliceTestClient!
.start()
.then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
})
.then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
})
.then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA);
return aliceTestClient!.httpBackend.flushAllExpected();
})
.then(function (): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", CURVE25519_KEY_BACKUP_DATA);
if (event.getContent()) {
return Promise.resolve(event);
}
// mock for the outgoing key requests that will be sent
fetchMock.put("express:/_matrix/client/r0/sendToDevice/m.room_key_request/:txid", {});
return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
})
.then((event) => {
expect(event.getContent()).toEqual("testytest");
// We'll need to add a signature to the backup data, so take a copy to avoid mutating global state.
const backupData = JSON.parse(JSON.stringify(CURVE25519_BACKUP_INFO));
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData);
aliceClient = await initTestClient();
await aliceClient.crypto!.signObject(backupData.auth_data);
await aliceClient.crypto!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY));
await aliceClient.crypto!.backupManager!.checkAndStart();
// start after saving the private key
await aliceClient.startClient();
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
expect(event.getContent()).toEqual("testytest");
});
});
+249 -20
View File
@@ -59,7 +59,7 @@ beforeAll(async () => {
// load the rust library. This can take a few seconds on a slow GH worker.
beforeAll(async () => {
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-js");
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
await RustSdkCryptoJs.initAsync();
}, 10000);
@@ -81,9 +81,9 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
*/
// we test with both crypto stacks...
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
/** the client under test */
let aliceClient: MatrixClient;
@@ -164,6 +164,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(requests[0].transactionId).toEqual(transactionId);
}
// check that the returned request depends on the given userID
{
const requests = aliceClient
.getCrypto()!
.getVerificationRequestsToDeviceInProgress("@unknown:localhost");
expect(requests.length).toEqual(0);
}
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
@@ -387,12 +395,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
});
oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
it("can verify another via QR code with an untrusted cross-signing key", async () => {
aliceClient = await startTestClient();
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
// have alice initiate a verification. She should send a m.key.verification.request
const [requestBody, request] = await Promise.all([
@@ -430,16 +437,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
);
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
// we should still be "Ready" and have no verifier
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync({
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.reciprocate.v1",
transaction_id: transactionId,
secret: encodeUnpaddedBase64(sharedSecret),
},
});
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
@@ -447,34 +450,184 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// there should now be a verifier
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
// ... which we call .verify on, which emits a ShowReciprocateQr event
const verificationPromise = verifier.verify();
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
const reciprocatePromise = new Promise<ShowQrCodeCallbacks>((resolve) => {
verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
});
const verificationPromise = verifier.verify();
const reciprocateQRCodeCallbacks = await reciprocatePromise;
// getReciprocateQrCodeCallbacks() is an alternative way to get the callbacks
expect(verifier.getReciprocateQrCodeCallbacks()).toBe(reciprocateQRCodeCallbacks);
expect(verifier.getShowSasCallbacks()).toBeNull();
// Alice confirms she is happy
// Alice confirms she is happy, which makes her reply with a 'done'
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.done");
reciprocateQRCodeCallbacks.confirm();
await sendToDevicePromise;
// that should satisfy Alice, who should reply with a 'done'
// the dummy device replies with its own 'done'
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
});
newBackendOnly("can verify another by scanning their QR code", async () => {
aliceClient = await startTestClient();
// we need cross-signing keys for a QR code verification
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
// Alice sends a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, indicating it can show a QR code
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.show.v1", "m.reciprocate.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.otherPartySupportsMethod("m.qr_code.show.v1")).toBe(true);
// the dummy device shows a QR code
const sharedSecret = "SUPERSEKRET";
const qrCodeBuffer = buildQRCode(
transactionId,
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
sharedSecret,
);
// Alice scans the QR code
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
const verifier = await request.scanQRCode(qrCodeBuffer);
const requestBody = await sendToDevicePromise;
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage).toEqual({
from_device: aliceClient.deviceId,
method: "m.reciprocate.v1",
transaction_id: transactionId,
secret: encodeUnpaddedBase64(Buffer.from(sharedSecret)),
});
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
const verificationPromise = verifier.verify();
// the dummy device confirms that Alice scanned the QR code, by replying with a done
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
// Alice also replies with a 'done'
await expectSendToDeviceMessage("m.key.verification.done");
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
});
it("can send an SAS start after QR code display", async () => {
aliceClient = await startTestClient();
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
// Alice sends a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, with an indication it can scan a QR code
// or do the emoji dance
returnToDeviceMessageFromSync(
buildReadyMessage(transactionId, ["m.qr_code.scan.v1", "m.sas.v1", "m.reciprocate.v1"]),
);
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// Alice displays the QR code
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
await request.startVerification("m.sas.v1");
await sendToDevicePromise;
// There should now be a `verifier`
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(request.chosenMethod).toEqual("m.sas.v1");
// clean up the test
expectSendToDeviceMessage("m.key.verification.cancel");
request.cancel();
await expect(verifier.verify()).rejects.toBeTruthy();
});
it("can receive an SAS start after QR code display", async () => {
aliceClient = await startTestClient();
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
// Alice sends a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, with an indication it can scan a QR code
// or do the emoji dance
returnToDeviceMessageFromSync(
buildReadyMessage(transactionId, ["m.qr_code.scan.v1", "m.sas.v1", "m.reciprocate.v1"]),
);
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// Alice displays the QR code
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
await emitPromise(request, VerificationRequestEvent.Change);
// Alice should now have a `verifier`
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(request.chosenMethod).toEqual("m.sas.v1");
// clean up the test
expectSendToDeviceMessage("m.key.verification.cancel");
request.cancel();
await expect(verifier.verify()).rejects.toBeTruthy();
});
});
describe("cancellation", () => {
beforeEach(async () => {
// pretend that we have another device, which we will start verifying
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
aliceClient = await startTestClient();
await waitForDeviceList();
@@ -542,6 +695,50 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.phase).toEqual(VerificationPhase.Cancelled);
expect(verifier.hasBeenCancelled).toBe(true);
});
it("can cancel in the ShowQrCodeCallbacks", async () => {
// have alice initiate a verification. She should send a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, with an indication it can scan the QR code
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.scan.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
// there should now be a verifier
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
// ... which we call .verify on, which emits a ShowReciprocateQr event
const reciprocatePromise = emitPromise(verifier, VerifierEvent.ShowReciprocateQr);
const verificationPromise = verifier.verify();
const reciprocateQRCodeCallbacks: ShowQrCodeCallbacks = await reciprocatePromise;
// Alice complains that she didn't see the dummy device scan her code
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.cancel");
reciprocateQRCodeCallbacks.cancel();
await sendToDevicePromise;
// ... which should cancel the verifier
await expect(verificationPromise).rejects.toBeTruthy();
expect(request.phase).toEqual(VerificationPhase.Cancelled);
expect(verifier.hasBeenCancelled).toBe(true);
});
});
describe("Incoming verification from another device", () => {
@@ -717,6 +914,19 @@ function buildReadyMessage(transactionId: string, methods: string[]): { type: st
};
}
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
return {
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.reciprocate.v1",
transaction_id: transactionId,
secret: encodeUnpaddedBase64(sharedSecret),
},
};
}
/** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */
function buildSasStartMessage(transactionId: string): { type: string; content: object } {
return {
@@ -794,3 +1004,22 @@ function buildDoneMessage(transactionId: string) {
},
};
}
function buildQRCode(transactionId: string, key1Base64: string, key2Base64: string, sharedSecret: string): Uint8Array {
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
const qrCodeBuffer = Buffer.alloc(150); // oversize
let idx = 0;
idx += qrCodeBuffer.write("MATRIX", idx, "ascii");
idx = qrCodeBuffer.writeUInt8(0x02, idx); // version
idx = qrCodeBuffer.writeUInt8(0x02, idx); // mode
idx = qrCodeBuffer.writeInt16BE(transactionId.length, idx);
idx += qrCodeBuffer.write(transactionId, idx, "ascii");
idx += Buffer.from(key1Base64, "base64").copy(qrCodeBuffer, idx);
idx += Buffer.from(key2Base64, "base64").copy(qrCodeBuffer, idx);
idx += qrCodeBuffer.write(sharedSecret, idx);
// truncate to the right length
return qrCodeBuffer.subarray(0, idx);
}
@@ -248,7 +248,7 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(function () {
const room = client.getRoom(roomId)!;
const timelineSet = room!.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -260,7 +260,18 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId)!;
const timelineSet = room!.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
event: {
event_id: "event",
},
events_after: [],
events_before: [],
state: [],
}));
return Promise.all([
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
httpBackend.flushAllExpected(),
]);
});
});
@@ -271,7 +282,7 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(function () {
const timelineSet = new EventTimelineSet(undefined);
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -778,7 +789,18 @@ describe("MatrixClient event timelines", function () {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId)!;
const timelineSet = room.getTimelineSets()[0];
expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy();
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
event: {
event_id: "event",
},
events_after: [],
events_before: [],
state: [],
}));
return Promise.all([
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
httpBackend.flushAllExpected(),
]);
});
});
@@ -1262,7 +1284,6 @@ describe("MatrixClient event timelines", function () {
THREAD_ROOT.event_id,
THREAD_REPLY.event_id,
THREAD_REPLY2.getId(),
THREAD_ROOT_REACTION.getId(),
THREAD_REPLY3.getId(),
]);
});
@@ -2005,6 +2026,25 @@ describe("MatrixClient event timelines", function () {
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when(
"GET",
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) +
"/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) +
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
)
.respond(200, function () {
return {
chunk: [THREAD_REPLY],
};
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
const room = client.getRoom(roomId)!;
File diff suppressed because it is too large Load Diff
+12 -11
View File
@@ -1161,11 +1161,6 @@ describe("SlidingSync", () => {
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
let txnId: string | undefined;
httpBackend!
@@ -1180,8 +1175,12 @@ describe("SlidingSync", () => {
txn_id: txnId,
};
});
await httpBackend!.flushAllExpected();
await okPromise;
await Promise.all([
expect(failPromise).rejects.toEqual(gotTxnIds[0]),
expect(failPromise2).rejects.toEqual(gotTxnIds[1]),
httpBackend!.flushAllExpected(),
okPromise,
]);
expect(txnId).toBeDefined();
});
@@ -1200,7 +1199,6 @@ describe("SlidingSync", () => {
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges("a", [[0, 20]]);
let pendingC = true;
@@ -1217,9 +1215,12 @@ describe("SlidingSync", () => {
txn_id: gotTxnIds[1],
};
});
await httpBackend!.flushAllExpected();
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
await Promise.all([
expect(A).rejects.toEqual(gotTxnIds[0]),
httpBackend!.flushAllExpected(),
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved
]);
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
+5 -2
View File
@@ -32,13 +32,16 @@ export function mockInitialApiRequests(homeserverUrl: string) {
/**
* Mock the requests needed to set up cross signing
*
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return 404 error for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
@@ -71,6 +71,29 @@ def main() -> None:
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_master_private_key = encode_base64(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_self_signing_private_key = encode_base64(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_user_signing_private_key = encode_base64(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
print(
f"""\
@@ -96,6 +119,21 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, ind
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(), indent=4)
+15
View File
@@ -36,6 +36,21 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "ZG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "c2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "dXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
+2 -2
View File
@@ -18,7 +18,7 @@ import { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({
@@ -34,7 +34,7 @@ export const makeThreadEvent = ({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
rel_type: THREAD_RELATION_TYPE.name,
["m.in_reply_to"]: {
event_id: replyToEventId,
},
+5 -7
View File
@@ -129,7 +129,7 @@ describe("DeviceList", function () {
});
});
it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
it("should have an outdated devicelist on an invalidation while an update is in progress", async function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList("@test1:sw1v.org");
@@ -148,11 +148,8 @@ describe("DeviceList", function () {
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
// TODO: Fix this test so we actually await the call and assertions and remove
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
//
// eslint-disable-next-line jest/valid-expect-in-promise
dl.saveIfDirty()
await dl
.saveIfDirty()
.then(() => {
// the first request completes
queryDefer1.resolve({
@@ -163,12 +160,13 @@ describe("DeviceList", function () {
});
return prom1;
})
.then(() => {
.then(async () => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
await dl2.load();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
+35 -4
View File
@@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { QueryDict } from "../../../src/utils";
import { defer, QueryDict } from "../../../src/utils";
import { logger } from "../../../src/logger";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
@@ -274,11 +277,13 @@ describe("FetchHttpApi", () => {
];
const runTests = (fetchBaseUrl: string) => {
it.each<TestCase>(testCases)(
"creates url with params %s",
({ path, queryParams, prefix, baseUrl }, result) => {
"creates url with params %s => %s",
({ path, queryParams, prefix, baseUrl }, expected) => {
const api = makeApi(fetchBaseUrl);
expect(api.getUrl(path, queryParams, prefix, baseUrl)).toEqual(new URL(result));
const result = api.getUrl(path, queryParams, prefix, baseUrl);
// we only check the stringified URL, to avoid having the test depend on the internals of URL.
expect(result.toString()).toEqual(expected);
},
);
};
@@ -290,4 +295,30 @@ describe("FetchHttpApi", () => {
runTests(baseUrlWithTrailingSlash);
});
});
it("should not log query parameters", async () => {
jest.useFakeTimers();
const deferred = defer<Response>();
const fetchFn = jest.fn().mockReturnValue(deferred.promise);
jest.spyOn(logger, "debug").mockImplementation(() => {});
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
jest.advanceTimersByTime(1234);
deferred.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
await prom;
expect(logger.debug).not.toHaveBeenCalledWith("fragment");
expect(logger.debug).not.toHaveBeenCalledWith("query");
expect(logger.debug).not.toHaveBeenCalledWith("param");
expect(logger.debug).toHaveBeenCalledTimes(2);
expect(mocked(logger.debug).mock.calls[0]).toMatchInlineSnapshot(`
[
"FetchHttpApi: --> GET https://server:8448/some/path?query=xxx",
]
`);
expect(mocked(logger.debug).mock.calls[1]).toMatchInlineSnapshot(`
[
"FetchHttpApi: <-- GET https://server:8448/some/path?query=xxx [1234ms 200]",
]
`);
});
});
+37 -1
View File
@@ -375,7 +375,7 @@ describe("InteractiveAuth", () => {
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
});
it("should handle unexpected error types without data propery set", async () => {
it("should handle unexpected error types without data property set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -559,4 +559,40 @@ describe("InteractiveAuth", () => {
ia.chooseStage();
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
});
it("should fire stateUpdated callback if with error when encountered", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
doRequest.mockRejectedValue(new MatrixError({ errcode: "M_UNKNOWN", error: "This is an error" }));
await Promise.allSettled([ia.attemptAuth()]);
expect(stateUpdated).toHaveBeenCalledWith("m.login.password", {
errcode: "M_UNKNOWN",
error: "This is an error",
});
});
});
+3 -3
View File
@@ -121,7 +121,7 @@ describe("refreshToken", () => {
body: { errcode: "M_UNRECOGNIZED" },
});
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ errcode: "M_UNRECOGNIZED" });
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ errcode: "M_UNRECOGNIZED" });
});
it("re-raises non-M_UNRECOGNIZED exceptions from /v3", async () => {
@@ -132,7 +132,7 @@ describe("refreshToken", () => {
throw new Error("/v1/refresh unexpectedly called");
});
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
});
it("re-raises non-M_UNRECOGNIZED exceptions from /v1", async () => {
@@ -144,6 +144,6 @@ describe("refreshToken", () => {
});
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), 429);
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
});
});
+5 -1
View File
@@ -703,7 +703,11 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
root.setThreadId(root.getId());
await room.addLiveEvents([root]);
return room.createThread(root.getId()!, root, [], false);
// Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false);
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
return thread;
}
/**
+2 -2
View File
@@ -66,7 +66,7 @@ describe("registerOidcClient()", () => {
fetchMockJest.post(registrationEndpoint, {
status: 500,
});
expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
OidcError.DynamicRegistrationFailed,
);
});
@@ -77,7 +77,7 @@ describe("registerOidcClient()", () => {
// no clientId in response
body: "{}",
});
expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
OidcError.DynamicRegistrationInvalid,
);
});
+1 -1
View File
@@ -657,7 +657,7 @@ describe("NotificationService", function () {
content: {
"body": "",
"msgtype": "m.text",
"org.matrix.msc3952.mentions": {},
"m.mentions": {},
},
});
+130 -32
View File
@@ -16,9 +16,9 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
import { MAIN_ROOM_TIMELINE, ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { MatrixClient } from "../../src/client";
import { EventType } from "../../src/matrix";
import { EventType, MatrixEvent, Room } from "../../src/matrix";
import { synthesizeReceipt } from "../../src/models/read-receipt";
import { encodeUri } from "../../src/utils";
import * as utils from "../test-utils/test-utils";
@@ -42,42 +42,46 @@ let httpBackend: MockHttpBackend;
const THREAD_ID = "$thread_event_id";
const ROOM_ID = "!123:matrix.org";
const threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
const roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
describe("Read receipt", () => {
let threadEvent: MatrixEvent;
let roomEvent: MatrixEvent;
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
userId: "@user:server",
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
client.isGuest = () => false;
client.supportsThreads = () => true;
threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
});
describe("sendReceipt", () => {
@@ -143,13 +147,69 @@ describe("Read receipt", () => {
await httpBackend.flushAllExpected();
await flushPromises();
});
it("should send a main timeline read receipt for a reaction to a thread root", async () => {
roomEvent.event.event_id = THREAD_ID;
const reaction = utils.mkReaction(roomEvent, client, client.getSafeUserId(), ROOM_ID);
const thread = new Room(ROOM_ID, client, client.getSafeUserId()).createThread(
THREAD_ID,
roomEvent,
[threadEvent],
false,
);
threadEvent.setThread(thread);
reaction.setThread(thread);
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: reaction.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
})
.respond(200, {});
client.sendReceipt(reaction, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("should always send unthreaded receipts if threads support is disabled", async () => {
client.supportsThreads = () => false;
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: roomEvent.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toEqual(undefined);
})
.respond(200, {});
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
});
describe("synthesizeReceipt", () => {
it.each([
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
{ getEvent: () => roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ getEvent: () => threadEvent, destinationId: THREAD_ID },
])("adds the receipt to $destinationId", ({ getEvent, destinationId }) => {
const event = getEvent();
const userId = "@bob:example.org";
const receiptType = ReceiptType.Read;
@@ -160,4 +220,42 @@ describe("Read receipt", () => {
expect(content.thread_id).toEqual(destinationId);
});
});
describe("addReceiptToStructure", () => {
it("should not allow an older unthreaded receipt to clobber a `main` threaded one", () => {
const userId = client.getSafeUserId();
const room = new Room(ROOM_ID, client, userId);
const unthreadedReceipt: WrappedReceipt = {
eventId: "$olderEvent",
data: {
ts: 1234567880,
},
};
const mainTimelineReceipt: WrappedReceipt = {
eventId: "$newerEvent",
data: {
ts: 1234567890,
},
};
room.addReceiptToStructure(
mainTimelineReceipt.eventId,
ReceiptType.ReadPrivate,
userId,
mainTimelineReceipt.data,
false,
);
expect(room.getEventReadUpTo(userId)).toBe(mainTimelineReceipt.eventId);
room.addReceiptToStructure(
unthreadedReceipt.eventId,
ReceiptType.ReadPrivate,
userId,
unthreadedReceipt.data,
false,
);
expect(room.getEventReadUpTo(userId)).toBe(mainTimelineReceipt.eventId);
});
});
});
+6 -7
View File
@@ -96,7 +96,7 @@ describe("ECDHv2", function () {
expect(aliceChecksum).toEqual(bobChecksum);
expect(alice.connect()).rejects.toThrow();
await expect(alice.connect()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
@@ -120,9 +120,9 @@ describe("ECDHv2", function () {
alice.close();
expect(alice.connect()).rejects.toThrow();
expect(alice.send({})).rejects.toThrow();
expect(alice.receive()).rejects.toThrow();
await expect(alice.connect()).rejects.toThrow();
await expect(alice.send({})).rejects.toThrow();
await expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
@@ -146,10 +146,10 @@ describe("ECDHv2", function () {
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(bob.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
await expect(bob.receive()).rejects.toThrow();
});
it("ciphertext before set up", async function () {
@@ -164,9 +164,8 @@ describe("ECDHv2", function () {
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await expect(alice.receive()).rejects.toThrow();
});
});
});
+4 -4
View File
@@ -172,7 +172,7 @@ describe("Rendezvous", function () {
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
await httpBackend.flush("");
expect(cancelPromise).resolves.toBeUndefined();
await expect(cancelPromise).resolves.toBeUndefined();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequests();
@@ -603,7 +603,7 @@ describe("Rendezvous", function () {
it("device not online within timeout", async function () {
const { aliceRz } = await completeLogin({});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("device appears online within timeout", async function () {
@@ -627,7 +627,7 @@ describe("Rendezvous", function () {
getFingerprint: () => "bbbb",
};
}, 1500);
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("mismatched device key", async function () {
@@ -636,6 +636,6 @@ describe("Rendezvous", function () {
getFingerprint: () => "XXXX",
},
});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
});
});
@@ -95,10 +95,10 @@ describe("SimpleHttpRendezvousTransport", function () {
httpBackend.verifyNoOutstandingExpectation();
}
}
it("should throw an error when no server available", function () {
it("should throw an error when no server available", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
});
it("POST to fallback server", async function () {
@@ -130,7 +130,6 @@ describe("SimpleHttpRendezvousTransport", function () {
fetchFn,
});
const prom = simpleHttpTransport.send({});
expect(prom).rejects.toThrow();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
@@ -138,7 +137,7 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("");
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
});
it("POST with absolute path response", async function () {
@@ -364,7 +363,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.details()).rejects.toThrow();
await expect(simpleHttpTransport.details()).rejects.toThrow();
});
it("send after cancelled", async function () {
@@ -375,7 +374,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fetchFn,
});
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
});
it("receive before ready", async function () {
@@ -385,7 +384,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.receive()).rejects.toThrow();
await expect(simpleHttpTransport.receive()).rejects.toThrow();
});
it("404 failure callback", async function () {
@@ -398,7 +397,6 @@ describe("SimpleHttpRendezvousTransport", function () {
onFailure,
});
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
@@ -406,7 +404,10 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("", 1);
await Promise.all([
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
httpBackend.flush("", 1),
]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
});
@@ -438,7 +439,6 @@ describe("SimpleHttpRendezvousTransport", function () {
}
{
// GET with 404 to simulate expiry
expect(simpleHttpTransport.receive()).resolves.toBeUndefined();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
@@ -446,7 +446,7 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("");
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
}
});
+76
View File
@@ -0,0 +1,76 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { EventType, MatrixClient, Room } from "../../src";
import { RoomHierarchy } from "../../src/room-hierarchy";
describe("RoomHierarchy", () => {
const roomId = "!room:server";
const client = new MatrixClient({ baseUrl: "https://server", userId: "@user:server" });
it("should load data from /hierarchy API", async () => {
const spy = fetchMock.getOnce(
`https://server/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/hierarchy?suggested_only=false`,
{
rooms: [],
},
{ overwriteRoutes: true },
);
const room = new Room(roomId, client, client.getSafeUserId());
const hierarchy = new RoomHierarchy(room);
const res = await hierarchy.load();
expect(spy).toHaveBeenCalled();
expect(res).toHaveLength(0);
});
describe("itSuggested", () => {
it("should return true if a room is suggested", async () => {
const spy = fetchMock.getOnce(
`https://server/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/hierarchy?suggested_only=false`,
{
rooms: [
{
children_state: [
{
origin_server_ts: 111,
content: {
suggested: true,
via: ["matrix.org"],
},
type: EventType.SpaceChild,
state_key: "!child_room:server",
},
],
room_id: roomId,
},
],
},
{ overwriteRoutes: true },
);
const room = new Room(roomId, client, client.getSafeUserId());
const hierarchy = new RoomHierarchy(room);
await hierarchy.load();
expect(spy).toHaveBeenCalled();
expect(hierarchy.isSuggested(hierarchy.root.roomId, "!child_room:server")).toBeTruthy();
});
});
});
+85 -34
View File
@@ -2556,7 +2556,7 @@ describe("Room", function () {
next_batch: "start_token",
});
let prom = emitPromise(room, ThreadEvent.New);
const prom = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread: Thread = await prom;
await emitPromise(room, ThreadEvent.Update);
@@ -2583,9 +2583,11 @@ describe("Room", function () {
},
});
prom = emitPromise(room, ThreadEvent.Update);
await room.addLiveEvents([threadResponseEdit]);
await prom;
// XXX: If we add the relation to the thread response before the thread finishes fetching via /relations
// then the test will fail
await emitPromise(room, ThreadEvent.Update);
await emitPromise(room, ThreadEvent.Update);
await Promise.all([emitPromise(room, ThreadEvent.Update), room.addLiveEvents([threadResponseEdit])]);
expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
});
@@ -2765,7 +2767,7 @@ describe("Room", function () {
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
count: 1,
current_user_participated: true,
},
},
@@ -2787,10 +2789,10 @@ describe("Room", function () {
let prom = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([threadRoot, threadResponse1]);
const thread: Thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread.initialEventsFetched).toBeTruthy();
await room.addLiveEvents([threadResponse2]);
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
@@ -2809,11 +2811,10 @@ describe("Room", function () {
},
});
prom = emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2);
await room.addLiveEvents([threadResponse2Redaction]);
await prom;
await emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2);
await emitPromise(room, ThreadEvent.Update);
await room.addLiveEvents([threadResponse2Redaction]);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent!.getId()).toBe(threadResponse1.getId());
@@ -2849,7 +2850,7 @@ describe("Room", function () {
Thread.setServerSideSupport(FeatureSupport.Stable);
const room = new Room(roomId, client, userA);
it("thread root and its relations&redactions should be in both", () => {
it("thread root and its relations&redactions should be in main timeline", () => {
const randomMessage = mkMessage();
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2867,6 +2868,9 @@ describe("Room", function () {
threadReaction2Redaction,
];
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
events.slice(1).forEach((ev) => ev.setThread(thread));
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(randomMessage, events, roots).shouldLiveInThread).toBeFalsy();
@@ -2878,14 +2882,11 @@ describe("Room", function () {
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
});
it("thread response and its relations&redactions should be only in thread timeline", () => {
@@ -2909,25 +2910,39 @@ describe("Room", function () {
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
});
it("reply to thread response and its relations&redactions should be only in main timeline", () => {
it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
const reply1 = mkReply(threadResponse1);
const reaction1 = utils.mkReaction(reply1, room.client, userA, roomId);
const reaction2 = utils.mkReaction(reply1, room.client, userA, roomId);
const reaction2Redaction = mkRedaction(reply1);
const threadResp1 = mkThreadResponse(threadRoot);
const threadResp1Reply1 = mkReply(threadResp1);
const threadResp1Reply1Reaction1 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
const threadResp1Reply1Reaction2 = utils.mkReaction(threadResp1Reply1, room.client, userA, roomId);
const thResp1Rep1React2Redaction = mkRedaction(threadResp1Reply1);
const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, reply1, reaction1, reaction2, reaction2Redaction];
const events = [
threadRoot,
threadResp1,
threadResp1Reply1,
threadResp1Reply1Reaction1,
threadResp1Reply1Reaction2,
thResp1Rep1React2Redaction,
];
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
events.forEach((ev) => ev.setThread(thread));
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResp1Reply1, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction1, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResp1Reply1Reaction2, events, roots).threadId).toBe(thread.id);
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(thResp1Rep1React2Redaction, events, roots).threadId).toBe(thread.id);
});
it("reply to reply to thread root should only be in the main timeline", () => {
@@ -2939,12 +2954,40 @@ describe("Room", function () {
const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, reply1, reply2];
const thread = room.createThread(threadRoot.getId()!, threadRoot, [], false);
threadResponse1.setThread(thread);
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
});
it("edit to thread root should live in main timeline only", () => {
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
const threadRootEdit = mkEdit(threadRoot);
threadRoot.makeReplaced(threadRootEdit);
const thread = room.createThread(threadRoot.getId()!, threadRoot, [threadResponse1], false);
threadResponse1.setThread(thread);
threadRootEdit.setThread(thread);
const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, threadRootEdit];
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadRoot, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadRoot, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadResponse1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadResponse1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(threadRootEdit, events, roots).shouldLiveInThread).toBeFalsy();
});
it("should aggregate relations in thread event timeline set", async () => {
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
@@ -2976,6 +3019,14 @@ describe("Room", function () {
expect(responseRelations![0][1].size).toEqual(1);
expect(responseRelations![0][1].has(threadReaction)).toBeTruthy();
});
it("a non-thread reply to an unknown parent event should live in the main timeline only", async () => {
const message = mkMessage(); // we do not add this message to any timelines
const reply = mkReply(message);
expect(room.eventShouldLiveIn(reply).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply).shouldLiveInThread).toBeFalsy();
});
});
describe("getEventReadUpTo()", () => {
@@ -3062,10 +3113,10 @@ describe("Room", function () {
it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as WrappedReceipt;
return { eventId: "eventId1", data: { ts: 123 } };
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as WrappedReceipt;
return { eventId: "eventId2", data: { ts: 123 } };
}
return null;
};
@@ -15,10 +15,11 @@ limitations under the License.
*/
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("CrossSigningIdentity", () => {
describe("bootstrapCrossSigning", () => {
@@ -31,6 +32,9 @@ describe("CrossSigningIdentity", () => {
/** A mock OutgoingRequestProcessor which crossSigning is connected to */
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
/** A mock ServerSideSecretStorage which crossSigning is connected to */
let secretStorage: Mocked<ServerSideSecretStorage>;
beforeEach(async () => {
await RustSdkCryptoJs.initAsync();
@@ -44,7 +48,11 @@ describe("CrossSigningIdentity", () => {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor);
secretStorage = {
get: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorage>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage, jest.fn());
});
it("should do nothing if keys are present on-device and in secret storage", async () => {
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "fetch-mock-jest";
import { Mocked } from "jest-mock";
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
@@ -16,7 +16,7 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import {
KeysBackupRequest,
KeysClaimRequest,
@@ -26,7 +26,7 @@ import {
SignatureUploadRequest,
SigningKeysUploadRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
} from "@matrix-org/matrix-sdk-crypto-wasm";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src";
+44 -51
View File
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-wasm";
import { Mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
@@ -34,7 +34,7 @@ import {
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { CryptoCallbacks, ImportRoomKeysOpts, VerificationRequest } from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
@@ -42,6 +42,10 @@ import * as testData from "../../test-utils/test-data";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
afterEach(() => {
fetchMock.reset();
});
describe("RustCrypto", () => {
describe(".importRoomKeys and .exportRoomKeys", () => {
let rustCrypto: RustCrypto;
@@ -164,7 +168,7 @@ describe("RustCrypto", () => {
});
});
it("getCrossSigningKeyId", async () => {
it("getCrossSigningKeyId when there is no cross signing keys", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
});
@@ -390,60 +394,39 @@ describe("RustCrypto", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto(undefined, testData.TEST_USER_ID);
});
afterEach(() => {
jest.useRealTimers();
});
it("returns false initially", async () => {
jest.useFakeTimers();
const prom = rustCrypto.userHasCrossSigningKeys();
// the getIdentity() request should wait for a /keys/query request to complete, but times out after 1500ms
await jest.advanceTimersByTimeAsync(2000);
await expect(prom).resolves.toBe(false);
});
it("returns false if there is no cross-signing identity", async () => {
// @ts-ignore private field
const olmMachine = rustCrypto.olmMachine;
const outgoingRequests: OutgoingRequest[] = await olmMachine.outgoingRequests();
// pick out the KeysQueryRequest, and respond to it with the device keys but *no* cross-signing keys.
const req = outgoingRequests.find((r) => r instanceof KeysQueryRequest)!;
await olmMachine.markRequestAsSent(
req.id!,
req.type,
JSON.stringify({
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
});
it("throws an error if the fetch fails", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", 400);
await expect(rustCrypto.userHasCrossSigningKeys()).rejects.toThrow("400 error");
});
it("returns false if the user has no cross-signing keys", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
});
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(false);
});
it("returns true if OlmMachine has a cross-signing identity", async () => {
// @ts-ignore private field
const olmMachine = rustCrypto.olmMachine;
it("returns true if the user has cross-signing keys", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
...testData.SIGNED_CROSS_SIGNING_KEYS_DATA,
});
const outgoingRequests: OutgoingRequest[] = await olmMachine.outgoingRequests();
// pick out the KeysQueryRequest, and respond to it with the cross-signing keys
const req = outgoingRequests.find((r) => r instanceof KeysQueryRequest)!;
await olmMachine.markRequestAsSent(
req.id!,
req.type,
JSON.stringify({
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
...testData.SIGNED_CROSS_SIGNING_KEYS_DATA,
}),
);
// ... and we should now have cross-signing keys.
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
});
});
@@ -520,6 +503,16 @@ describe("RustCrypto", () => {
);
});
});
describe("get|storeSessionBackupPrivateKey", () => {
it("can save and restore a key", async () => {
const key = "testtesttesttesttesttesttesttest";
const rustCrypto = await makeTestRustCrypto();
await rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key));
const fetched = await rustCrypto.getSessionBackupPrivateKey();
expect(new TextDecoder().decode(fetched!)).toEqual(key);
});
});
});
/** build a basic RustCrypto instance for testing
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Mocked } from "jest-mock";
import { RustVerificationRequest } from "../../../src/rust-crypto/verification";
@@ -87,7 +87,7 @@ function makeTestRequest(
): RustVerificationRequest {
inner ??= makeMockedInner();
outgoingRequestProcessor ??= {} as OutgoingRequestProcessor;
return new RustVerificationRequest(inner, outgoingRequestProcessor, undefined);
return new RustVerificationRequest(inner, outgoingRequestProcessor, []);
}
/** Mock up a rust-side VerificationRequest */
+1 -1
View File
@@ -278,7 +278,7 @@ describe("IndexedDBStore", () => {
workerFactory: () => worker,
});
await store.startup();
await expect(store.destroy()).resolves;
await store.destroy();
expect(terminate).toHaveBeenCalled();
});
});
-2
View File
@@ -38,8 +38,6 @@ import { mkMessage } from "../test-utils/test-utils";
import { makeBeaconEvent } from "../test-utils/beacon";
import { ReceiptType } from "../../src/@types/read_receipts";
// TODO: Fix types throughout
describe("utils", function () {
describe("encodeParams", function () {
it("should url encode and concat with &s", function () {
+1 -1
View File
@@ -1607,7 +1607,7 @@ describe("Call", function () {
it("throws when there is no error listener", async () => {
call.off(CallEvent.Error, errorListener);
expect(call.placeVoiceCall()).rejects.toThrow();
await expect(call.placeVoiceCall()).rejects.toThrow();
});
describe("hasPeerConnection()", () => {
+2 -2
View File
@@ -137,8 +137,8 @@ export enum PushRuleKind {
export enum RuleId {
Master = ".m.rule.master",
IsUserMention = ".org.matrix.msc3952.is_user_mention",
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
IsUserMention = ".m.rule.is_user_mention",
IsRoomMention = ".m.rule.is_room_mention",
ContainsDisplayName = ".m.rule.contains_display_name",
ContainsUserName = ".m.rule.contains_user_name",
AtRoomNotification = ".m.rule.roomnotif",
+16
View File
@@ -96,4 +96,20 @@ declare global {
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
interface RequestInit {
/**
* Specifies the priority of the fetch request relative to other requests of the same type.
* Must be one of the following strings:
* high: A high priority fetch request relative to other requests of the same type.
* low: A low priority fetch request relative to other requests of the same type.
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
*
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
* @see https://github.com/microsoft/TypeScript/issues/54472
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
* Not yet supported in Safari or Firefox
*/
priority?: "high" | "low" | "auto";
}
}
+6
View File
@@ -181,6 +181,12 @@ export interface IBindThreePidBody {
sid: string;
}
export interface IAddThreePidBody {
client_secret: string;
id_server: string;
sid: string;
}
export interface IRelationsRequestOpts {
from?: string;
to?: string;
+83 -52
View File
@@ -21,7 +21,7 @@ limitations under the License.
import { Optional } from "matrix-events-sdk";
import type { IDeviceKeys, IMegolmSessionData, IOneTimeKey } from "./@types/crypto";
import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync";
import { ISyncStateData, SetPresence, SyncApi, SyncApiOptions, SyncState } from "./sync";
import {
EventStatus,
IContent,
@@ -71,6 +71,7 @@ import {
UploadResponse,
HTTPError,
IRequestOpts,
Body,
} from "./http-api";
import {
Crypto,
@@ -132,6 +133,7 @@ import {
IFilterResponse,
ITagsResponse,
IStatusResponse,
IAddThreePidBody,
} from "./@types/requests";
import {
EventType,
@@ -208,7 +210,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
import { CryptoBackend } from "./common-crypto/CryptoBackend";
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
import { BootstrapCrossSigningOpts, CryptoApi, ImportRoomKeysOpts } from "./crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CryptoApi, ImportRoomKeysOpts } from "./crypto-api";
import { DeviceInfoMap } from "./crypto/DeviceList";
import {
AddSecretStorageKeyOpts,
@@ -522,13 +524,8 @@ export interface Capabilities {
[UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability;
}
/* eslint-disable camelcase */
export interface ICrossSigningKey {
keys: { [algorithm: string]: string };
signatures?: ISignatures;
usage: string[];
user_id: string;
}
/** @deprecated prefer {@link CrossSigningKeyInfo}. */
export type ICrossSigningKey = CrossSigningKeyInfo;
enum CrossSigningKeyType {
MasterKey = "master_key",
@@ -2012,12 +2009,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Set whether this client is a guest account. <b>This method is experimental
* and may change without warning.</b>
* @param guest - True if this is a guest account.
* @experimental if the token is a macaroon, it should be encoded in it that it is a 'guest'
* access token, which means that the SDK can determine this entirely without
* the dev manually flipping this flag.
*/
public setGuest(guest: boolean): void {
// EXPERIMENTAL:
// If the token is a macaroon, it should be encoded in it that it is a 'guest'
// access token, which means that the SDK can determine this entirely without
// the dev manually flipping this flag.
this.isGuestAccount = guest;
}
@@ -2241,7 +2237,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.cryptoCallbacks,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
);
rustCrypto.supportedVerificationMethods = this.verificationMethods;
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
this.cryptoBackend = rustCrypto;
@@ -2249,7 +2245,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
// re-emit the events emitted by the crypto impl
this.reEmitter.reEmit(rustCrypto, [CryptoEvent.VerificationRequestReceived]);
this.reEmitter.reEmit(rustCrypto, [
CryptoEvent.VerificationRequestReceived,
CryptoEvent.UserTrustStatusChanged,
]);
}
/**
@@ -2678,12 +2677,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
* @param opts - ICheckOwnCrossSigningTrustOpts object
*
* @deprecated Unneeded for the new crypto
*/
public checkOwnCrossSigningTrust(opts?: ICheckOwnCrossSigningTrustOpts): Promise<void> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.checkOwnCrossSigningTrust(opts);
return this.cryptoBackend.checkOwnCrossSigningTrust(opts);
}
/**
@@ -2854,12 +2855,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*
* @deprecated Prefer {@link CryptoApi.createRecoveryKeyFromPassphrase | `CryptoApi.createRecoveryKeyFromPassphrase`}.
*/
public createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.createRecoveryKeyFromPassphrase(password);
return this.cryptoBackend.createRecoveryKeyFromPassphrase(password);
}
/**
@@ -2874,7 +2877,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* return true.
*
* @returns True if secret storage is ready to be used on this device
* @deprecated Prefer {@link CryptoApi.isSecretStorageReady | `CryptoApi.isSecretStorageReady`}:
* @deprecated Prefer {@link CryptoApi.isSecretStorageReady | `CryptoApi.isSecretStorageReady`}.
*/
public isSecretStorageReady(): Promise<boolean> {
if (!this.cryptoBackend) {
@@ -2896,13 +2899,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* @deprecated Use {@link CryptoApi#bootstrapSecretStorage}.
* @deprecated Use {@link CryptoApi.bootstrapSecretStorage | `CryptoApi.bootstrapSecretStorage`}.
*/
public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.bootstrapSecretStorage(opts);
return this.cryptoBackend.bootstrapSecretStorage(opts);
}
/**
@@ -3718,10 +3721,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
const privKey = await this.crypto.getSessionBackupPrivateKey();
const privKey = await this.cryptoBackend.getSessionBackupPrivateKey();
if (!privKey) {
throw new Error("Couldn't get key");
}
@@ -3759,7 +3762,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const cacheCompleteCallback = opts?.cacheCompleteCallback;
const progressCallback = opts?.progressCallback;
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
@@ -3782,9 +3785,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }));
}
if (!(privKey instanceof Uint8Array)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`restoreKeyBackup expects Uint8Array, got ${privKey}`);
}
// Cache the key, if possible.
// This is async.
this.crypto
this.cryptoBackend
.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
logger.warn("Error caching session backup key:", e);
@@ -3841,7 +3848,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
algorithm.free();
}
await this.getCrypto()?.importRoomKeys(keys, {
await this.cryptoBackend.importRoomKeys(keys, {
progressCallback,
untrusted,
source: "backup",
@@ -4403,7 +4410,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private sendCompleteEvent(
roomId: string,
threadId: string | null,
eventObject: any,
eventObject: Partial<IEvent>,
txnId?: string,
): Promise<ISendEventResponse> {
if (!txnId) {
@@ -4980,7 +4987,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: to an empty object `{}`
* @returns Rejects: with an error response.
*/
public async sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, unthreaded = false): Promise<{}> {
public async sendReceipt(
event: MatrixEvent,
receiptType: ReceiptType,
body?: Record<string, any>,
unthreaded = false,
): Promise<{}> {
if (this.isGuest()) {
return Promise.resolve({}); // guests cannot send receipts so don't bother.
}
@@ -4991,9 +5003,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: event.getId()!,
});
if (!unthreaded) {
// A thread cannot be just a thread root and a thread root can only be read in the main timeline
const isThread = !!event.threadRootId && !event.isThreadRoot;
if (!unthreaded && this.supportsThreads()) {
// XXX: the spec currently says a threaded read receipt can be sent for the root of a thread,
// but in practice this isn't possible and the spec needs updating.
const isThread =
!!event.threadRootId &&
// A thread cannot be just a thread root and a thread root can only be read in the main timeline
!event.isThreadRoot &&
// Similarly non-thread relations upon the thread root (reactions, edits) should also be for the main timeline.
event.isRelation() &&
(event.isRelation(THREAD_RELATION_TYPE.name) || event.relationEventId !== event.threadRootId);
body = {
...body,
// Only thread replies should define a specific thread. Thread roots can only be read in the main timeline.
@@ -5120,6 +5139,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
undefined,
{
prefix: MediaPrefix.R0,
priority: "low",
},
);
// TODO: Expire the URL preview cache sometimes
@@ -5140,7 +5160,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$roomId: roomId,
$userId: this.getUserId()!,
});
const data: any = {
const data: QueryDict = {
typing: isTyping,
};
if (isTyping) {
@@ -5332,7 +5352,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const populationResults: { [roomId: string]: Error } = {};
const promises: Promise<any>[] = [];
const promises: Promise<unknown>[] = [];
const doLeave = (roomId: string): Promise<void> => {
return this.leave(roomId)
@@ -5533,6 +5553,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
}
/**
* Specify the set_presence value to be used for subsequent calls to the Sync API.
* This has an advantage over calls to the PUT /presence API in that it
* doesn't clobber status_msg set by other devices.
* @param presence - the presence to specify to set_presence of sync calls
*/
public async setSyncPresence(presence?: SetPresence): Promise<void> {
this.syncApi?.setPresence(presence);
}
/**
* @param opts - Options to apply
* @returns Promise which resolves
@@ -5925,7 +5955,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("getLatestTimeline only supports room timelines");
}
let event;
let event: IRoomEvent | undefined;
if (timelineSet.threadListType !== null) {
const res = await this.createThreadListMessagesRequest(
timelineSet.room.roomId,
@@ -6384,7 +6414,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
"",
);
let readPromise: Promise<any> = Promise.resolve<any>(undefined);
let readPromise: Promise<unknown> = Promise.resolve();
if (opts.allowRead) {
readPromise = this.sendStateEvent(
roomId,
@@ -6589,7 +6619,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
private async requestTokenFromEndpoint<T extends IRequestTokenResponse>(
endpoint: string,
params: Record<string, any>,
params: QueryDict,
): Promise<T> {
const postParams = Object.assign({}, params);
@@ -7939,8 +7969,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* to false.
* @returns Promise which resolves: On success, the empty object
*/
public deactivateAccount(auth?: any, erase?: boolean): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
const body: any = {};
public deactivateAccount(
auth?: AuthDict,
erase?: boolean,
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
const body: Body = {};
if (auth) {
body.auth = auth;
}
@@ -8178,7 +8211,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public sendStateEvent(
roomId: string,
eventType: string,
content: any,
content: IContent,
stateKey = "",
opts: IRequestOpts = {},
): Promise<ISendEventResponse> {
@@ -8396,13 +8429,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* it invisible.
* @returns Promise which resolves: result object
* @returns Rejects: with an error response.
* @deprecated missing from the spec
*/
public setRoomDirectoryVisibilityAppService(
networkId: string,
roomId: string,
visibility: "public" | "private",
): Promise<any> {
// TODO: Types
const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
$networkId: networkId,
$roomId: roomId,
@@ -8418,7 +8451,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an array of results.
*/
public searchUserDirectory({ term, limit }: { term: string; limit?: number }): Promise<IUserDirectoryResponse> {
const body: any = {
const body: Body = {
search_term: term,
};
@@ -8496,14 +8529,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Add a 3PID to your homeserver account and optionally bind it to an identity
* server as well. An identity server is required as part of the `creds` object.
*
* This API is deprecated, and you should instead use `addThreePidOnly`
* for homeservers that support it.
* @deprecated this API is deprecated, and you should instead use `addThreePidOnly` for homeservers that support it.
*
* @returns Promise which resolves: on success
* @returns Rejects: with an error response.
*/
public addThreePid(creds: any, bind: boolean): Promise<any> {
// TODO: Types
public addThreePid(creds: IAddThreePidBody, bind: boolean): Promise<{ submit_url?: string }> {
const path = "/account/3pid";
const data = {
threePidCreds: creds,
@@ -8663,7 +8694,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$device_id: deviceId,
});
const body: any = {};
const body: Body = {};
if (auth) {
body.auth = auth;
@@ -8681,7 +8712,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Rejects: with an error response.
*/
public deleteMultipleDevices(devices: string[], auth?: AuthDict): Promise<{}> {
const body: any = { devices };
const body: Body = { devices };
if (auth) {
body.auth = auth;
@@ -8858,7 +8889,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
{ body, next_batch: nextBatch }: { body: ISearchRequestBody; next_batch?: string },
abortSignal?: AbortSignal,
): Promise<ISearchResponse> {
const queryParams: any = {};
const queryParams: QueryDict = {};
if (nextBatch) {
queryParams.next_batch = nextBatch;
}
@@ -9478,7 +9509,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* response to getThirdpartyProtocols()
* @returns Promise which resolves to the result object
*/
public getThirdpartyUser(protocol: string, params: any): Promise<IThirdPartyUser[]> {
public getThirdpartyUser(protocol: string, params?: QueryDict): Promise<IThirdPartyUser[]> {
const path = utils.encodeUri("/thirdparty/user/$protocol", {
$protocol: protocol,
});
@@ -9654,7 +9685,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
proxyBaseUrl?: string,
abortSignal?: AbortSignal,
): Promise<MSC3575SlidingSyncResponse> {
const qps: Record<string, any> = {};
const qps: QueryDict = {};
if (req.pos) {
qps.pos = req.pos;
delete req.pos;
+25 -1
View File
@@ -24,6 +24,8 @@ import { IEventDecryptionResult } from "../@types/crypto";
/**
* Common interface for the crypto implementations
*
* @internal
*/
export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
/**
@@ -88,9 +90,21 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @returns the cross signing information for the user.
*/
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
/**
* Check the cross signing trust of the current user
*
* @param opts - Options object.
*
* @deprecated Unneeded for the new crypto
*/
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>;
}
/** The methods which crypto implementations should expose to the Sync api */
/** The methods which crypto implementations should expose to the Sync api
*
* @internal
*/
export interface SyncCryptoCallbacks {
/**
* Called by the /sync loop whenever there are incoming to-device messages.
@@ -146,6 +160,9 @@ export interface SyncCryptoCallbacks {
onSyncCompleted(syncState: OnSyncCompletedData): void;
}
/**
* @internal
*/
export interface OnSyncCompletedData {
/**
* The 'next_batch' result from /sync, which will become the 'since' token for the next call to /sync.
@@ -157,3 +174,10 @@ export interface OnSyncCompletedData {
*/
catchingUp?: boolean;
}
/**
* Options object for {@link CryptoBackend#checkOwnCrossSigningTrust}.
*/
export interface CheckOwnCrossSigningTrustOpts {
allowPrivateKeyRequests?: boolean;
}
+36
View File
@@ -21,6 +21,7 @@ import { UIAuthCallback } from "./interactive-auth";
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { KeyBackupInfo } from "./crypto-api/keybackup";
import { ISignatures } from "./@types/signed";
/**
* Public interface to the cryptography parts of the js-sdk
@@ -284,6 +285,30 @@ export interface CryptoApi {
* @returns a VerificationRequest when the request has been sent to the other party.
*/
requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Secure key backup
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Fetch the backup decryption key we have saved in our store.
*
* This can be used for gossiping the key to other devices.
*
* @returns the key, if any, or null
*/
getSessionBackupPrivateKey(): Promise<Uint8Array | null>;
/**
* Store the backup decryption key.
*
* This should be called if the client has received the key from another device via secret sharing (gossiping).
*
* @param key - the backup decryption key
*/
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;
}
/**
@@ -479,6 +504,17 @@ export enum CrossSigningKey {
UserSigning = "user_signing",
}
/**
* Information on one of the cross-signing keys.
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
*/
export interface CrossSigningKeyInfo {
keys: { [algorithm: string]: string };
signatures?: ISignatures;
usage: string[];
user_id: string;
}
/**
* Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase}
*/
+17 -2
View File
@@ -136,12 +136,27 @@ export interface VerificationRequest
/**
* Send an `m.key.verification.start` event to start verification via a particular method.
*
* This is normally used when starting a verification via emojis (ie, `method` is set to `m.sas.v1`).
*
* @param method - the name of the verification method to use.
*
* @returns The verifier which will do the actual verification.
*/
startVerification(method: string): Promise<Verifier>;
/**
* Start a QR code verification by providing a scanned QR code for this verification flow.
*
* Validates the QR code, and if it is ok, sends an `m.key.verification.start` event with `method` set to
* `m.reciprocate.v1`, to tell the other side the scan was successful.
*
* See also {@link VerificationRequest#startVerification} which can be used to start other verification methods.
*
* @param qrCodeData - the decoded QR code.
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
*/
scanQRCode(qrCodeData: Uint8Array): Promise<Verifier>;
/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.
@@ -295,7 +310,7 @@ export enum VerifierEvent {
ShowSas = "show_sas",
/**
* QR code data should be displayed to the user.
* The user should confirm if the other side has scanned our QR code.
*
* The payload is the {@link ShowQrCodeCallbacks} object.
*/
@@ -310,7 +325,7 @@ export type VerifierEventHandlerMap = {
};
/**
* Callbacks for user actions while a QR code is displayed.
* Callbacks for user actions to confirm that the other side has scanned our QR code.
*
* This is exposed as the payload of a `VerifierEvent.ShowReciprocateQr` event, or can be retrieved directly from the
* verifier as `reciprocateQREvent`.
+4 -5
View File
@@ -18,8 +18,7 @@ limitations under the License.
* Cross signing methods
*/
import { PkSigning } from "@matrix-org/olm";
import type { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from "./olmlib";
import { logger } from "../logger";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
@@ -245,7 +244,7 @@ export class CrossSigningInfo {
* @returns A map from key type (string) to private key (Uint8Array)
*/
public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
const keys = new Map();
const keys = new Map<string, Uint8Array>();
const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) {
@@ -294,8 +293,8 @@ export class CrossSigningInfo {
const privateKeys: Record<string, Uint8Array> = {};
const keys: Record<string, ICrossSigningKey> = {};
let masterSigning;
let masterPub;
let masterSigning: PkSigning | undefined;
let masterPub: string | undefined;
try {
if (level & CrossSigningLevel.MASTER) {
+1 -1
View File
@@ -188,7 +188,7 @@ export class EncryptionSetupOperation {
// We must only call `uploadDeviceSigningKeys` from inside this auth
// helper to ensure we properly handle auth errors.
await this.crossSigningKeys.authUpload?.((authDict) => {
return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
return baseApis.uploadDeviceSigningKeys(authDict ?? undefined, keys as CrossSigningKeys);
});
// pass the new keys to the main instance of our own CrossSigningInfo.
+11 -10
View File
@@ -48,7 +48,7 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors";
import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
import { DehydrationManager } from "./dehydration";
import { BackupManager } from "./backup";
import { IStore } from "../store";
@@ -1242,21 +1242,22 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns the key, if any, or null
*/
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
let key = await new Promise<any>((resolve) => {
// TODO types
const encodedKey = await new Promise<Uint8Array | IEncryptedPayload | string | null>((resolve) => {
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
});
let key: Uint8Array | null = null;
// make sure we have a Uint8Array, rather than a string
if (key && typeof key === "string") {
key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key));
if (typeof encodedKey === "string") {
key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(encodedKey) || encodedKey));
await this.storeSessionBackupPrivateKey(key);
}
if (key && key.ciphertext) {
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
const decrypted = await decryptAES(encodedKey, pickleKey, "m.megolm_backup.v1");
key = olmlib.decodeBase64(decrypted);
}
return key;
@@ -2202,12 +2203,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
blocked: boolean | null = null,
known: boolean | null = null,
keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo> {
): Promise<DeviceInfo | CrossSigningInfo | ICrossSigningKey | 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.
const xsk = this.deviceList.getStoredCrossSigningForUser(userId);
if (xsk && xsk.getId() === deviceId) {
if (xsk?.getId() === deviceId) {
if (blocked !== null || known !== null) {
throw new Error("Cannot set blocked or known for a cross-signing key");
}
@@ -2257,7 +2258,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// This will emit events when it comes back down the sync
// (we could do local echo to speed things up)
}
return device as any; // TODO types
return device!;
} else {
return xsk;
}
@@ -478,6 +478,10 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
return verifier;
}
public scanQRCode(qrCodeData: Uint8Array): Promise<Verifier> {
throw new Error("QR code scanning not supported by legacy crypto");
}
/**
* sends the initial .request event.
* @returns resolves when the event has been sent.
+3 -1
View File
@@ -97,7 +97,7 @@ export interface ICapabilities {
*/
export class RoomWidgetClient extends MatrixClient {
private room?: Room;
private widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
private readonly widgetApiReady: Promise<void>;
private lifecycle?: AbortController;
private syncState: SyncState | null = null;
@@ -109,6 +109,8 @@ export class RoomWidgetClient extends MatrixClient {
) {
super(opts);
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
// Request capabilities for the functionality this client needs to support
if (
capabilities.sendEvent?.length ||
+1
View File
@@ -63,6 +63,7 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
},
[Feature.IntentionalMentions]: {
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
matrixVersion: "v1.7",
},
};
+33 -4
View File
@@ -22,11 +22,10 @@ import { checkObjectHasKeys, encodeParams } from "../utils";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { Method } from "./method";
import { ConnectionError, MatrixError } from "./errors";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts, Body } from "./interface";
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
import { QueryDict } from "../utils";
type Body = Record<string, any> | BodyInit;
import { logger } from "../logger";
interface TypedResponse<T> extends Response {
json(): Promise<T>;
@@ -223,8 +222,11 @@ export class FetchHttpApi<O extends IHttpOpts> {
method: Method,
url: URL | string,
body?: Body,
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal"> = {},
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal" | "priority"> = {},
): Promise<ResponseType<T, O>> {
const urlForLogs = this.sanitizeUrlForLogs(url);
logger.debug(`FetchHttpApi: --> ${method} ${urlForLogs}`);
const headers = Object.assign({}, opts.headers || {});
const json = opts.json ?? true;
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
@@ -260,6 +262,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
const { signal, cleanup } = anySignal(signals);
let res: Response;
const start = Date.now();
try {
res = await this.fetch(url, {
signal,
@@ -273,8 +276,12 @@ export class FetchHttpApi<O extends IHttpOpts> {
cache: "no-cache",
credentials: "omit", // we send credentials via headers
keepalive: keepAlive,
priority: opts.priority,
});
logger.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${res.status}]`);
} catch (e) {
logger.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${e}]`);
if ((<Error>e).name === "AbortError") {
throw e;
}
@@ -293,6 +300,28 @@ export class FetchHttpApi<O extends IHttpOpts> {
return res as ResponseType<T, O>;
}
private sanitizeUrlForLogs(url: URL | string): string {
try {
let asUrl: URL;
if (typeof url === "string") {
asUrl = new URL(url);
} else {
asUrl = url;
}
// Remove the values of any URL params that could contain potential secrets
const sanitizedQs = new URLSearchParams();
for (const key of asUrl.searchParams.keys()) {
sanitizedQs.append(key, "xxx");
}
const sanitizedQsString = sanitizedQs.toString();
const sanitizedQsUrlPiece = sanitizedQsString ? `?${sanitizedQsString}` : "";
return asUrl.origin + asUrl.pathname + sanitizedQsUrlPiece;
} catch (error) {
// defensive coding for malformed url
return "??";
}
}
/**
* Form and return a homeserver request URL based on the given path params and prefix.
* @param path - The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
+3 -1
View File
@@ -16,6 +16,8 @@ limitations under the License.
import { MatrixError } from "./errors";
export type Body = Record<string, any> | BodyInit;
export interface IHttpOpts {
fetchFn?: typeof global.fetch;
@@ -31,7 +33,7 @@ export interface IHttpOpts {
localTimeoutMs?: number;
}
export interface IRequestOpts {
export interface IRequestOpts extends Pick<RequestInit, "priority"> {
/**
* The alternative base url to use.
* If not specified, uses this.opts.baseUrl
+14 -5
View File
@@ -159,11 +159,12 @@ export class NoAuthFlowFoundError extends Error {
* The type of an application callback to perform the user-interactive bit of UIA.
*
* It is called with a single parameter, `makeRequest`, which is a function which takes the UIA parameters and
* makes the HTTP request.
* makes the HTTP request. The `authData` parameter in `makeRequest` can be set to null to omit the `auth` field
* from the UIA request.
*
* The generic parameter `T` is the type of the response of the endpoint, once it is eventually successful.
*/
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict) => Promise<UIAResponse<T>>) => Promise<T>;
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict | null) => Promise<UIAResponse<T>>) => Promise<T>;
interface IOpts<T> {
/**
@@ -263,8 +264,8 @@ export class InteractiveAuth<T> {
private readonly requestEmailTokenCallback: IOpts<T>["requestEmailToken"];
private readonly supportedStages?: Set<string>;
// The current latest data received from the server during the user interactive auth flow.
private data: IAuthData;
// The current latest data or error received from the server during the user interactive auth flow.
private data: IAuthData & MatrixError["data"];
private emailSid?: string;
private requestingEmailToken = false;
private attemptAuthDeferred: IDeferred<T> | null = null;
@@ -548,7 +549,7 @@ export class InteractiveAuth<T> {
matrixError.data.session = (this.data as IAuthData).session;
}
if (matrixError) {
this.data = matrixError.data as IAuthData;
this.data = matrixError.data;
}
try {
this.startNextAuthStage();
@@ -601,6 +602,14 @@ export class InteractiveAuth<T> {
return;
}
if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data?.errcode || "",
error: this.data?.error || "",
});
return;
}
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
}
+14 -1
View File
@@ -63,9 +63,22 @@ export * as SecretStorage from "./secret-storage";
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
export { createNewMatrixCall } from "./webrtc/call";
export type { MatrixCall } from "./webrtc/call";
export { GroupCallEvent, GroupCallIntent, GroupCallState, GroupCallType } from "./webrtc/groupCall";
export {
GroupCallEvent,
GroupCallIntent,
GroupCallState,
GroupCallType,
GroupCallStatsReportEvent,
} from "./webrtc/groupCall";
export type { GroupCall } from "./webrtc/groupCall";
export { CryptoEvent } from "./crypto";
export { SlidingSyncEvent } from "./sliding-sync";
export { MediaHandlerEvent } from "./webrtc/mediaHandler";
export { CallEvent } from "./webrtc/call";
export { CallFeedEvent } from "./webrtc/callFeed";
export { StatsReport } from "./webrtc/stats/statsReport";
export { RelationsEvent } from "./models/relations";
export { LocalStorageErrors } from "./store/local-storage-events-emitter";
/**
* Types supporting cryptography.
+10 -1
View File
@@ -984,11 +984,20 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
);
}
const { threadId, shouldLiveInRoom } = this.room.eventShouldLiveIn(event);
const { threadId, shouldLiveInRoom, shouldLiveInThread } = this.room.eventShouldLiveIn(event);
if (this.thread) {
return this.thread.id === threadId;
}
if (!shouldLiveInRoom && !shouldLiveInThread) {
logger.warn(
`EventTimelineSet:canContain event encountered which cannot be added to any timeline roomId=${
this.room?.roomId
} eventId=${event.getId()} threadId=${event.threadRootId}`,
);
}
return shouldLiveInRoom;
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ export interface IContent {
"displayname"?: string;
"m.relates_to"?: IEventRelation;
"org.matrix.msc3952.mentions"?: IMentions;
"m.mentions"?: IMentions;
}
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
+21 -19
View File
@@ -57,9 +57,10 @@ export abstract class ReadReceipt<
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
// Map: receipt type → user Id → receipt
private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>(
() => new Map(),
);
private receipts = new MapWithDefault<
string,
Map<string, [realReceipt: WrappedReceipt | null, syntheticReceipt: WrappedReceipt | null]>
>(() => new Map());
private receiptCacheByEventId: ReceiptCache = new Map();
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
@@ -85,6 +86,13 @@ export abstract class ReadReceipt<
return syntheticReceipt ?? realReceipt;
}
private compareReceipts(a: WrappedReceipt, b: WrappedReceipt): number {
// Try compare them in our unfiltered timeline set order, falling back to receipt timestamp which should be
// relatively sane as receipts are set only by the originating homeserver so as long as its clock doesn't
// jump around then it should be valid.
return this.getUnfilteredTimelineSet().compareEventOrdering(a.eventId, b.eventId) ?? a.data.ts - b.data.ts;
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
@@ -99,19 +107,13 @@ export abstract class ReadReceipt<
// receipt type here again. IMHO this should be done by the server in
// some more intelligent manner or the client should just use timestamps
const timelineSet = this.getUnfilteredTimelineSet();
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
// If we have both, compare them
let comparison: number | null | undefined;
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
}
// If we didn't get a comparison try to compare the ts of the receipts
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
comparison = this.compareReceipts(publicReadReceipt, privateReadReceipt);
}
// The public receipt is more likely to drift out of date so the private
@@ -142,20 +144,20 @@ export abstract class ReadReceipt<
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
}
if (existingReceipt) {
// we only want to add this receipt if we think it is later than the one we already have.
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId);
if (ordering !== null && ordering >= 0) {
return;
}
}
const wrappedReceipt: WrappedReceipt = {
eventId,
data: receipt,
};
if (existingReceipt) {
// We only want to add this receipt if we think it is later than the one we already have.
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
const ordering = this.compareReceipts(existingReceipt, wrappedReceipt);
if (ordering >= 0) {
return;
}
}
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
+35 -23
View File
@@ -2093,6 +2093,14 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
}
/**
* Determine which timeline(s) a given event should live in
* Thread roots live in both the main timeline and their corresponding thread timeline
* Relations, redactions, replies to thread relation events live only in the thread timeline
* Relations (other than m.thread), redactions, replies to a thread root live only in the main timeline
* Relations, redactions, replies where the parent cannot be found live in no timelines but should be aggregated regardless.
* Otherwise, the event lives in the main timeline only.
*/
public eventShouldLiveIn(
event: MatrixEvent,
events?: MatrixEvent[],
@@ -2109,7 +2117,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
};
}
// A thread root is always shown in both timelines
// A thread root is the only event shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId()!)) {
return {
shouldLiveInRoom: true,
@@ -2118,8 +2126,29 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
};
}
// A thread relation (1st and 2nd order) is always only shown in a thread
const isThreadRelation = event.isRelation(THREAD_RELATION_TYPE.name);
const parentEventId = event.getAssociatedId();
const threadRootId = event.threadRootId;
// Where the parent is the thread root and this is a non-thread relation this should live only in the main timeline
if (!!parentEventId && !isThreadRelation && (threadRootId === parentEventId || roots?.has(parentEventId!))) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
let parentEvent: MatrixEvent | undefined;
if (parentEventId) {
parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
}
// Treat non-thread-relations, redactions, and replies as extensions of their parents so evaluate parentEvent instead
if (parentEvent && !isThreadRelation) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
// A thread relation (1st and 2nd order) is always only shown in a thread
if (threadRootId != undefined) {
return {
shouldLiveInRoom: false,
@@ -2128,33 +2157,16 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
};
}
const parentEventId = event.getAssociatedId();
let parentEvent: MatrixEvent | undefined;
if (parentEventId) {
parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
}
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
if (parentEvent && (event.isRelation() || event.isRedaction())) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
if (!event.isRelation()) {
// Due to replies not being typical relations and being used as fallbacks for threads relations
// If we bypass the if case above then we know we are not a thread, so if we are still a reply
// then we know that we must be in the main timeline. Same goes if we have no associated parent event.
if (!parentEventId || !!event.replyEventId) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
// Edge case where we know the event is a relation but don't have the parentEvent
if (roots?.has(event.relationEventId!)) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// We've exhausted all scenarios,
// we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
// adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts
-5
View File
@@ -326,11 +326,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
this.setEventMetadata(event);
if (!this.initialEventsFetched && !toStartOfTimeline && event.getId() === this.id) {
// We're loading the thread organically
this.initialEventsFetched = true;
}
const lastReply = this.lastReply();
const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp;
+3 -3
View File
@@ -78,7 +78,7 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
conditions: [
{
kind: ConditionKind.EventPropertyContains,
key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
key: "content.m\\.mentions.user_ids",
value: "", // The user ID is dynamically added in rewriteDefaultRules.
},
],
@@ -91,7 +91,7 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
conditions: [
{
kind: ConditionKind.EventPropertyIs,
key: "content.org\\.matrix\\.msc3952\\.mentions.room",
key: "content.m\\.mentions.room",
value: true,
},
{
@@ -724,7 +724,7 @@ export class PushProcessor {
// Disable the deprecated mentions push rules if the new mentions property exists.
if (
this.client.supportsIntentionalMentions() &&
ev.getContent()["org.matrix.msc3952.mentions"] !== undefined &&
ev.getContent()["m.mentions"] !== undefined &&
(rule.rule_id === RuleId.ContainsUserName ||
rule.rule_id === RuleId.ContainsDisplayName ||
rule.rule_id === RuleId.AtRoomNotification)
+10 -3
View File
@@ -17,7 +17,12 @@ limitations under the License.
import { UnstableValue } from "matrix-events-sdk";
import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
import { IMSC3882GetLoginTokenCapability, MatrixClient, UNSTABLE_MSC3882_CAPABILITY } from "../client";
import {
ICrossSigningKey,
IMSC3882GetLoginTokenCapability,
MatrixClient,
UNSTABLE_MSC3882_CAPABILITY,
} from "../client";
import { CrossSigningInfo } from "../crypto/CrossSigning";
import { DeviceInfo } from "../crypto/deviceinfo";
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
@@ -178,7 +183,9 @@ export class MSC3906Rendezvous {
return deviceId;
}
private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo): Promise<CrossSigningInfo | DeviceInfo> {
private async verifyAndCrossSignDevice(
deviceInfo: DeviceInfo,
): Promise<CrossSigningInfo | DeviceInfo | ICrossSigningKey | undefined> {
if (!this.client.crypto) {
throw new Error("Crypto not available on client");
}
@@ -223,7 +230,7 @@ export class MSC3906Rendezvous {
*/
public async verifyNewDeviceOnExistingDevice(
timeout = 10 * 1000,
): Promise<DeviceInfo | CrossSigningInfo | undefined> {
): Promise<DeviceInfo | CrossSigningInfo | ICrossSigningKey | undefined> {
if (!this.newDeviceId) {
throw new Error("No new device to sign");
}
+34 -3
View File
@@ -14,19 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OlmMachine, CrossSigningStatus } from "@matrix-org/matrix-sdk-crypto-js";
import { OlmMachine, CrossSigningStatus } from "@matrix-org/matrix-sdk-crypto-wasm";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { BootstrapCrossSigningOpts } from "../crypto-api";
import { logger } from "../logger";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { UIAuthCallback } from "../interactive-auth";
import { ServerSideSecretStorage } from "../secret-storage";
/** Manages the cross-signing keys for our own user.
*
* @internal
*/
export class CrossSigningIdentity {
public constructor(
private readonly olmMachine: OlmMachine,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly secretStorage: ServerSideSecretStorage,
/** Called if the cross signing keys are imported from the secret storage */
private readonly onCrossSigningKeysImport: () => void,
) {}
/**
@@ -39,7 +46,15 @@ export class CrossSigningIdentity {
}
const olmDeviceStatus: CrossSigningStatus = await this.olmMachine.crossSigningStatus();
const privateKeysInSecretStorage = false; // TODO
// Try to fetch cross signing keys from the secret storage
const masterKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.master");
const selfSigningKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.self_signing");
const userSigningKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.user_signing");
const privateKeysInSecretStorage = Boolean(
masterKeyFromSecretStorage && selfSigningKeyFromSecretStorage && userSigningKeyFromSecretStorage,
);
const olmDeviceHasKeys =
olmDeviceStatus.hasMaster && olmDeviceStatus.hasUserSigning && olmDeviceStatus.hasSelfSigning;
@@ -65,7 +80,23 @@ export class CrossSigningIdentity {
"bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
throw new Error("TODO");
await this.olmMachine.importCrossSigningKeys(
masterKeyFromSecretStorage,
selfSigningKeyFromSecretStorage,
userSigningKeyFromSecretStorage,
);
// Get the current device
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice(
this.olmMachine.userId,
this.olmMachine.deviceId,
);
// Sign the device with our cross-signing key and upload the signature
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
this.onCrossSigningKeysImport();
}
// TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the
+3 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
@@ -22,6 +22,8 @@ import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
* KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races
*
* We have one of these per `RustCrypto` (and hence per `MatrixClient`).
*
* @internal
*/
export class KeyClaimManager {
private currentClaimPromise: Promise<void>;
+10 -4
View File
@@ -24,7 +24,7 @@ import {
SignatureUploadRequest,
ToDeviceRequest,
SigningKeysUploadRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
} from "@matrix-org/matrix-sdk-crypto-wasm";
import { logger } from "../logger";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
@@ -34,6 +34,8 @@ import { UIAResponse } from "../@types/uia";
/**
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
*
* @internal
*/
export interface OutgoingRequest {
readonly id: string | undefined;
@@ -49,6 +51,8 @@ export interface OutgoingRequest {
* * holding the reference to the `MatrixHttpApi`
* * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them
* * sending the results of such requests back to the rust backend.
*
* @internal
*/
export class OutgoingRequestProcessor {
public constructor(
@@ -112,11 +116,13 @@ export class OutgoingRequestProcessor {
}
const parsedBody = JSON.parse(body);
const makeRequest = async (auth: IAuthDict): Promise<UIAResponse<T>> => {
const newBody = {
const makeRequest = async (auth: IAuthDict | null): Promise<UIAResponse<T>> => {
const newBody: Record<string, any> = {
...parsedBody,
auth,
};
if (auth !== null) {
newBody.auth = auth;
}
const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody));
return JSON.parse(resp) as T;
};
+3 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
import { EventType } from "../@types/event";
import { IContent, MatrixEvent } from "../models/event";
@@ -26,6 +26,8 @@ import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
/**
* RoomEncryptor: responsible for encrypting messages to a given room
*
* @internal
*/
export class RoomEncryptor {
private readonly prefixedLogger: PrefixedLogger;
+7 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Device, DeviceVerification } from "../models/device";
import { DeviceKeys } from "../client";
@@ -23,6 +23,8 @@ import { DeviceKeys } from "../client";
* Convert a {@link RustSdkCryptoJs.Device} to a {@link Device}
* @param device - Rust Sdk device
* @param userId - owner of the device
*
* @internal
*/
export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: RustSdkCryptoJs.UserId): Device {
// Copy rust device keys to Device.keys
@@ -84,6 +86,8 @@ export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: Rus
/**
* Convert {@link DeviceKeys} from `/keys/query` request to a `Map<string, Device>`
* @param deviceKeys - Device keys object to convert
*
* @internal
*/
export function deviceKeysToDeviceMap(deviceKeys: DeviceKeys): Map<string, Device> {
return new Map(
@@ -97,6 +101,8 @@ type QueryDevice = DeviceKeys[keyof DeviceKeys];
/**
* Convert `/keys/query` {@link QueryDevice} device to {@link Device}
* @param device - Device from `/keys/query` request
*
* @internal
*/
export function downloadDeviceToJsDevice(device: QueryDevice): Device {
const keys = new Map(Object.entries(device.keys));
+4 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { RustCrypto } from "./rust-crypto";
import { logger } from "../logger";
@@ -33,6 +33,8 @@ import { ICryptoCallbacks } from "../crypto";
* @param cryptoCallbacks - Crypto callbacks provided by the application
* @param storePrefix - the prefix to use on the indexeddbs created by rust-crypto.
* If unset, a memory store will be used.
*
* @internal
*/
export async function initRustCrypto(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
@@ -42,7 +44,7 @@ export async function initRustCrypto(
cryptoCallbacks: ICryptoCallbacks,
storePrefix: string | null,
): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
// initialise the rust matrix-sdk-crypto-wasm, if it hasn't already been done
await RustSdkCryptoJs.initAsync();
// enable tracing in the rust-sdk
+151 -23
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
@@ -41,6 +41,7 @@ import {
ImportRoomKeyProgressData,
ImportRoomKeysOpts,
VerificationRequest,
CrossSigningKeyInfo,
} from "../crypto-api";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
@@ -56,8 +57,12 @@ import { EventType } from "../@types/event";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
*
* @internal
*/
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
public globalErrorOnUnknownDevices = false;
@@ -104,7 +109,17 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.eventDecryptor = new EventDecryptor(olmMachine);
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor);
// Fire if the cross signing keys are imported from the secret storage
const onCrossSigningKeysImport = (): void => {
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(this.userId));
};
this.crossSigningIdentity = new CrossSigningIdentity(
olmMachine,
this.outgoingRequestProcessor,
secretStorage,
onCrossSigningKeysImport,
);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -190,6 +205,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return null;
}
/**
* This function is unneeded for the rust-crypto.
* The cross signing key import and the device verification are done in {@link CryptoApi#bootstrapCrossSigning}
*
* The function is stub to keep the compatibility with the old crypto.
* More information: https://github.com/vector-im/element-web/issues/25648
*
*
* Implementation of {@link CryptoBackend#checkOwnCrossSigningTrust}
*/
public async checkOwnCrossSigningTrust(): Promise<void> {
return;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// CryptoApi implementation
@@ -202,7 +231,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
*/
public async userHasCrossSigningKeys(): Promise<boolean> {
const userIdentity = await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
const userId = new RustSdkCryptoJs.UserId(this.userId);
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
*/
const request = this.olmMachine.queryKeysForUsers([userId]);
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
const userIdentity = await this.olmMachine.getIdentity(userId);
return userIdentity !== undefined;
}
@@ -286,15 +321,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
*/
private async getUserDevices(userId: string): Promise<Map<string, Device>> {
const rustUserId = new RustSdkCryptoJs.UserId(userId);
const devices: RustSdkCryptoJs.UserDevices = await this.olmMachine.getUserDevices(rustUserId);
return new Map(
devices
.devices()
.map((device: RustSdkCryptoJs.Device) => [
device.deviceId.toString(),
rustDeviceToJsDevice(device, rustUserId),
]),
);
// For reasons I don't really understand, the Javascript FinalizationRegistry doesn't seem to run the
// registered callbacks when `userDevices` goes out of scope, nor when the individual devices in the array
// returned by `userDevices.devices` do so.
//
// This is particularly problematic, because each of those structures holds a reference to the
// VerificationMachine, which in turn holds a reference to the IndexeddbCryptoStore. Hence, we end up leaking
// open connections to the crypto store, which means the store can't be deleted on logout.
//
// To fix this, we explicitly call `.free` on each of the objects, which tells the rust code to drop the
// allocated memory and decrement the refcounts for the crypto store.
const userDevices: RustSdkCryptoJs.UserDevices = await this.olmMachine.getUserDevices(rustUserId);
try {
const deviceArray: RustSdkCryptoJs.Device[] = userDevices.devices();
try {
return new Map(
deviceArray.map((device) => [device.deviceId.toString(), rustDeviceToJsDevice(device, rustUserId)]),
);
} finally {
deviceArray.forEach((d) => d.free());
}
} finally {
userDevices.free();
}
}
/**
@@ -367,8 +418,45 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
*/
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
// TODO
return null;
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus();
const privateKeysOnDevice =
crossSigningStatus.hasMaster && crossSigningStatus.hasUserSigning && crossSigningStatus.hasSelfSigning;
if (!userIdentity || !privateKeysOnDevice) {
// The public or private keys are not available on this device
return null;
}
if (!userIdentity.isVerified()) {
// We have both public and private keys, but they don't match!
return null;
}
let key: string;
switch (type) {
case CrossSigningKey.Master:
key = userIdentity.masterKey;
break;
case CrossSigningKey.SelfSigning:
key = userIdentity.selfSigningKey;
break;
case CrossSigningKey.UserSigning:
key = userIdentity.userSigningKey;
break;
default:
// Unknown type
return null;
}
const parsedKey: CrossSigningKeyInfo = JSON.parse(key);
// `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.
return Object.values(parsedKey.keys)[0];
}
/**
@@ -549,7 +637,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
*/
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
const requests: RustSdkCryptoJs.VerificationRequest[] = this.olmMachine.getVerificationRequests(
new RustSdkCryptoJs.UserId(this.userId),
new RustSdkCryptoJs.UserId(userId),
);
return requests
.filter((request) => request.roomId === undefined)
@@ -558,7 +646,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
new RustVerificationRequest(
request,
this.outgoingRequestProcessor,
this.supportedVerificationMethods,
this._supportedVerificationMethods,
),
);
}
@@ -580,10 +668,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
/**
* The verification methods we offer to the other side during an interactive verification.
*/
private _supportedVerificationMethods: string[] = ALL_VERIFICATION_METHODS;
/**
* Set the verification methods we offer to the other side during an interactive verification.
*
* If `undefined`, we will offer all the methods supported by the Rust SDK.
*/
public supportedVerificationMethods: string[] | undefined;
public setSupportedVerificationMethods(methods: string[] | undefined): void {
// by default, the Rust SDK does not offer `m.qr_code.scan.v1`, but we do want to offer that.
this._supportedVerificationMethods = methods ?? ALL_VERIFICATION_METHODS;
}
/**
* Send a verification request to our other devices.
@@ -604,10 +700,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await userIdentity.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}
/**
@@ -634,10 +730,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await device.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
}
/**
* Fetch the backup decryption key we have saved in our store.
*
* Implementation of {@link CryptoApi#getSessionBackupPrivateKey}.
*
* @returns the key, if any, or null
*/
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
if (!backupKeys.decryptionKeyBase64) return null;
return Buffer.from(backupKeys.decryptionKeyBase64, "base64");
}
/**
* Store the backup decryption key.
*
* Implementation of {@link CryptoApi#storeSessionBackupPrivateKey}.
*
* @param key - the backup decryption key
*/
public async storeSessionBackupPrivateKey(key: Uint8Array): Promise<void> {
const base64Key = Buffer.from(key).toString("base64");
// TODO get version from backupManager
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -791,7 +914,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
if (request) {
this.emit(
CryptoEvent.VerificationRequestReceived,
new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods),
new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods),
);
}
}
@@ -985,11 +1108,16 @@ class EventDecryptor {
}
}
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived;
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged;
type RustCryptoEventMap = {
/**
* Fires when a key verification request is received.
*/
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
/**
* Fires when the cross signing keys are imported during {@link CryptoApi#bootstrapCrossSigning}
*/
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserTrustLevel) => void;
};
+2
View File
@@ -21,6 +21,8 @@ import { ServerSideSecretStorage } from "../secret-storage";
*
* @param secretStorage - The secret store using account data
* @returns True if the cross-signing keys are all stored and encrypted with the same secret storage key.
*
* @internal
*/
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
// Check if the master cross-signing key is stored in secret storage
+270 -85
View File
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import { Emoji } from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Emoji, QrState } from "@matrix-org/matrix-sdk-crypto-wasm";
import {
ShowQrCodeCallbacks,
@@ -30,21 +30,27 @@ import {
} from "../crypto-api/verification";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { TypedReEmitter } from "../ReEmitter";
/**
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
*
* @internal
*/
export class RustVerificationRequest
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
implements VerificationRequest
{
/** a reëmitter which relays VerificationRequestEvent.Changed events emitted by the verifier */
private readonly reEmitter: TypedReEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>;
/** Are we in the process of sending an `m.key.verification.ready` event? */
private _accepting = false;
/** Are we in the process of sending an `m.key.verification.cancellation` event? */
private _cancelling = false;
private _verifier: Verifier | undefined;
private _verifier: undefined | RustSASVerifier | RustQrCodeVerifier;
/**
* Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`.
@@ -56,18 +62,23 @@ export class RustVerificationRequest
public constructor(
private readonly inner: RustSdkCryptoJs.VerificationRequest,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly supportedVerificationMethods: string[] | undefined,
private readonly supportedVerificationMethods: string[],
) {
super();
this.reEmitter = new TypedReEmitter(this);
const onChange = async (): Promise<void> => {
// if we now have a `Verification` where we lacked one before, wrap it.
// TODO: QR support
if (this._verifier === undefined) {
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
if (verification instanceof RustSdkCryptoJs.Sas) {
this._verifier = new RustSASVerifier(verification, this, outgoingRequestProcessor);
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
// If we now have a `Verification` where we lacked one before, or we have transitioned from QR to SAS,
// wrap the new rust Verification as a js-sdk Verifier.
if (verification instanceof RustSdkCryptoJs.Sas) {
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor));
}
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
}
this.emit(VerificationRequestEvent.Change);
@@ -75,6 +86,15 @@ export class RustVerificationRequest
inner.registerChangesCallback(onChange);
}
private setVerifier(verifier: RustSASVerifier | RustQrCodeVerifier): void {
// if we already have a verifier, unsubscribe from its events
if (this._verifier) {
this.reEmitter.stopReEmitting(this._verifier, [VerificationRequestEvent.Change]);
}
this._verifier = verifier;
this.reEmitter.reEmit(this._verifier, [VerificationRequestEvent.Change]);
}
/**
* Unique ID for this verification request.
*
@@ -131,7 +151,11 @@ export class RustVerificationRequest
// parlance.
return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready;
case RustSdkCryptoJs.VerificationRequestPhase.Transitioned:
return VerificationPhase.Started;
if (!this._verifier) {
// this shouldn't happen, because the onChange handler should have created a _verifier.
throw new Error("VerificationRequest: inner phase == Transitioned but no verifier!");
}
return this._verifier.verificationPhase;
case RustSdkCryptoJs.VerificationRequestPhase.Done:
return VerificationPhase.Done;
case RustSdkCryptoJs.VerificationRequestPhase.Cancelled:
@@ -182,10 +206,13 @@ export class RustVerificationRequest
/** the method picked in the .start event */
public get chosenMethod(): string | null {
if (this.phase !== VerificationPhase.Started) return null;
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
// TODO: this isn't quite right. The existence of a Verification doesn't prove that we have .started.
if (verification instanceof RustSdkCryptoJs.Sas) {
return "m.sas.v1";
} else if (verification instanceof RustSdkCryptoJs.Qr) {
return "m.reciprocate.v1";
} else {
return null;
}
@@ -223,12 +250,9 @@ export class RustVerificationRequest
this._accepting = true;
try {
const req: undefined | OutgoingRequest =
this.supportedVerificationMethods === undefined
? this.inner.accept()
: this.inner.acceptWithMethods(
this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
const req: undefined | OutgoingRequest = this.inner.acceptWithMethods(
this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
);
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
@@ -313,20 +337,51 @@ export class RustVerificationRequest
return this._verifier;
}
/**
* Start a QR code verification by providing a scanned QR code for this verification flow.
*
* Implementation of {@link Crypto.VerificationRequest#scanQRCode}.
*
* @param qrCodeData - the decoded QR code.
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
*/
public async scanQRCode(uint8Array: Uint8Array): Promise<Verifier> {
const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(new Uint8ClampedArray(uint8Array));
const verifier: RustSdkCryptoJs.Qr = await this.inner.scanQrCode(scan);
// this should have triggered the onChange callback, and we should now have a verifier
if (!this._verifier) {
throw new Error("Still no verifier after scanQrCode() call");
}
// we can immediately trigger the reciprocate request
const req: undefined | OutgoingRequest = verifier.reciprocate();
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
return this._verifier;
}
/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.
*/
public get verifier(): Verifier | undefined {
return this._verifier;
// It's possible for us to have a Verifier before a method has been chosen (in particular,
// if we are showing a QR code which the other device has not yet scanned. At that point, we could
// still switch to SAS).
//
// In that case, we should not return it to the application yet, since the application will not expect the
// Verifier to be replaced during the lifetime of the VerificationRequest.
return this.phase === VerificationPhase.Started ? this._verifier : undefined;
}
/**
* Stub implementation of {@link Crypto.VerificationRequest#getQRCodeBytes}.
*/
public getQRCodeBytes(): Buffer | undefined {
// TODO
return undefined;
throw new Error("getQRCodeBytes() unsupported in Rust Crypto; use generateQRCode() instead.");
}
/**
@@ -335,8 +390,8 @@ export class RustVerificationRequest
* Implementation of {@link Crypto.VerificationRequest#generateQRCode}.
*/
public async generateQRCode(): Promise<Buffer | undefined> {
// TODO
return undefined;
const innerVerifier: RustSdkCryptoJs.Qr = await this.inner.generateQrCode();
return Buffer.from(innerVerifier.toBytes());
}
/**
@@ -357,22 +412,28 @@ export class RustVerificationRequest
}
}
export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> implements Verifier {
/** Common base class for `Verifier` implementations which wrap rust classes.
*
* The generic parameter `InnerType` is the type of the rust Verification class which we wrap.
*
* @internal
*/
abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas> extends TypedEventEmitter<
VerifierEvent | VerificationRequestEvent,
VerifierEventHandlerMap & VerificationRequestEventHandlerMap
> {
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
private readonly completionPromise: Promise<void>;
private callbacks: ShowSasCallbacks | null = null;
protected readonly completionPromise: Promise<void>;
public constructor(
private readonly inner: RustSdkCryptoJs.Sas,
_verificationRequest: RustVerificationRequest,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
protected readonly inner: InnerType,
protected readonly outgoingRequestProcessor: OutgoingRequestProcessor,
) {
super();
this.completionPromise = new Promise<void>((resolve, reject) => {
const onChange = async (): Promise<void> => {
this.updateCallbacks();
this.onChange();
if (this.inner.isDone()) {
resolve(undefined);
@@ -386,6 +447,8 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
),
);
}
this.emit(VerificationRequestEvent.Change);
};
inner.registerChangesCallback(onChange);
});
@@ -393,8 +456,178 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
this.completionPromise.catch(() => null);
}
/**
* Hook which is called when the underlying rust class notifies us that there has been a change.
*
* Can be overridden by subclasses to see if we can notify the application about an update.
*/
protected onChange(): void {}
/**
* Returns true if the verification has been cancelled, either by us or the other side.
*/
public get hasBeenCancelled(): boolean {
return this.inner.isCancelled();
}
/**
* The ID of the other user in the verification process.
*/
public get userId(): string {
return this.inner.otherUserId.toString();
}
/**
* Cancel a verification.
*
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
*
* @param e - the reason for the cancellation.
*/
public cancel(e?: Error): void {
// TODO: something with `e`
const req: undefined | OutgoingRequest = this.inner.cancel();
if (req) {
this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
}
/**
* Get the details for an SAS verification, if one is in progress
*
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
* the SAS matches.
*/
public getShowSasCallbacks(): ShowSasCallbacks | null {
return null;
}
/**
* Get the details for reciprocating QR code verification, if one is in progress
*
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
* already scanned our QR code), and we are waiting for the user to confirm.
*/
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
return null;
}
}
/** A Verifier instance which is used to show and/or scan a QR code. */
export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> implements Verifier {
private callbacks: ShowQrCodeCallbacks | null = null;
public constructor(inner: RustSdkCryptoJs.Qr, outgoingRequestProcessor: OutgoingRequestProcessor) {
super(inner, outgoingRequestProcessor);
}
protected onChange(): void {
// if the other side has scanned our QR code and sent us a "reciprocate" message, it is now time for the
// application to prompt the user to confirm their side.
if (this.callbacks === null && this.inner.hasBeenScanned()) {
this.callbacks = {
confirm: () => this.confirmScanning(),
cancel: () => this.cancel(),
};
}
}
/**
* Start the key verification, if it has not already been started.
*
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
* or times out.
*/
public async verify(): Promise<void> {
// Some applications (hello, matrix-react-sdk) may not check if there is a `ShowQrCodeCallbacks` and instead
// register a `ShowReciprocateQr` listener which they expect to be called once `.verify` is called.
if (this.callbacks !== null) {
this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks);
}
// Nothing to do here but wait.
await this.completionPromise;
}
/**
* Calculate an appropriate VerificationPhase for a VerificationRequest where this is the verifier.
*
* This is abnormally complicated because a rust-side QR Code verifier can span several verification phases.
*/
public get verificationPhase(): VerificationPhase {
switch (this.inner.state()) {
case QrState.Created:
// we have created a QR for display; neither side has yet sent an `m.key.verification.start`.
return VerificationPhase.Ready;
case QrState.Scanned:
// other side has scanned our QR and sent an `m.key.verification.start` with `m.reciprocate.v1`
return VerificationPhase.Started;
case QrState.Confirmed:
// we have confirmed the other side's scan and sent an `m.key.verification.done`.
return VerificationPhase.Done;
case QrState.Reciprocated:
// although the rust SDK doesn't immediately send the `m.key.verification.start` on transition into this
// state, `RustVerificationRequest.scanQrCode` immediately calls `reciprocate()` and does so, so in practice
// we can treat the two the same.
return VerificationPhase.Started;
case QrState.Done:
return VerificationPhase.Done;
case QrState.Cancelled:
return VerificationPhase.Cancelled;
default:
throw new Error(`Unknown qr code state ${this.inner.state()}`);
}
}
/**
* Get the details for reciprocating QR code verification, if one is in progress
*
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
* already scanned our QR code), and we are waiting for the user to confirm.
*/
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
return this.callbacks;
}
private async confirmScanning(): Promise<void> {
const req: undefined | OutgoingRequest = this.inner.confirmScanning();
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
}
}
/** A Verifier instance which is used if we are exchanging emojis */
export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implements Verifier {
private callbacks: ShowSasCallbacks | null = null;
public constructor(
inner: RustSdkCryptoJs.Sas,
_verificationRequest: RustVerificationRequest,
outgoingRequestProcessor: OutgoingRequestProcessor,
) {
super(inner, outgoingRequestProcessor);
}
/**
* Start the key verification, if it has not already been started.
*
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
* if the other side has already sent a start event.
*
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
* or times out.
*/
public async verify(): Promise<void> {
const req: undefined | OutgoingRequest = this.inner.accept();
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
await this.completionPromise;
}
/** if we can now show the callbacks, do so */
private updateCallbacks(): void {
protected onChange(): void {
if (this.callbacks === null) {
const emoji: Array<Emoji> | undefined = this.inner.emoji();
const decimal = this.inner.decimals() as [number, number, number] | undefined;
@@ -426,50 +659,10 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
}
/**
* Returns true if the verification has been cancelled, either by us or the other side.
* Calculate an appropriate VerificationPhase for a VerificationRequest where this is the verifier.
*/
public get hasBeenCancelled(): boolean {
return this.inner.isCancelled();
}
/**
* The ID of the other user in the verification process.
*/
public get userId(): string {
return this.inner.otherUserId.toString();
}
/**
* Start the key verification, if it has not already been started.
*
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
* if the other side has already sent a start event.
*
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
* or times out.
*/
public async verify(): Promise<void> {
const req: undefined | OutgoingRequest = this.inner.accept();
if (req) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
await this.completionPromise;
}
/**
* Cancel a verification.
*
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
*
* @param e - the reason for the cancellation.
*/
public cancel(e: Error): void {
// TODO: something with `e`
const req: undefined | OutgoingRequest = this.inner.cancel();
if (req) {
this.outgoingRequestProcessor.makeOutgoingRequest(req);
}
public get verificationPhase(): VerificationPhase {
return VerificationPhase.Started;
}
/**
@@ -481,16 +674,6 @@ export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEv
public getShowSasCallbacks(): ShowSasCallbacks | null {
return this.callbacks;
}
/**
* Get the details for reciprocating QR code verification, if one is in progress
*
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
* already scanned our QR code), and we are waiting for the user to confirm.
*/
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
return null;
}
}
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
@@ -507,6 +690,8 @@ const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.Verificati
* @param method - specced method identifier, for example `m.sas.v1`.
* @returns Rust-side `VerificationMethod` corresponding to `method`.
* @throws An error if the method is unknown.
*
* @internal
*/
export function verificationMethodIdentifierToMethod(method: string): RustSdkCryptoJs.VerificationMethod {
const meth = verificationMethodsByIdentifier[method];
+9
View File
@@ -27,6 +27,7 @@ import {
SyncApiOptions,
defaultClientOpts,
defaultSyncApiOpts,
SetPresence,
} from "./sync";
import { MatrixEvent } from "./models/event";
import { Crypto } from "./crypto";
@@ -463,6 +464,14 @@ export class SlidingSyncSdk {
// TODO
}
/**
* Specify the set_presence value to be used for subsequent calls to the Sync API.
* @param presence - the presence to specify to set_presence of sync calls
*/
public setPresence(presence?: SetPresence): void {
// TODO not possible in sliding sync yet
}
/**
* Returns the current state of this sync object
* @see MatrixClient#event:"sync"
+12 -1
View File
@@ -168,7 +168,7 @@ export interface ISyncStateData {
fromCache?: boolean;
}
enum SetPresence {
export enum SetPresence {
Offline = "offline",
Online = "online",
Unavailable = "unavailable",
@@ -225,6 +225,7 @@ export class SyncApi {
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
private failedSyncCount = 0; // Number of consecutive failed /sync requests
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
private presence?: SetPresence;
/**
* Construct an entity which is able to sync with a homeserver.
@@ -1009,6 +1010,8 @@ export class SyncApi {
if (this.opts.disablePresence) {
qps.set_presence = SetPresence.Offline;
} else if (this.presence !== undefined) {
qps.set_presence = this.presence;
}
if (syncToken) {
@@ -1031,6 +1034,14 @@ export class SyncApi {
return qps;
}
/**
* Specify the set_presence value to be used for subsequent calls to the Sync API.
* @param presence - the presence to specify to set_presence of sync calls
*/
public setPresence(presence?: SetPresence): void {
this.presence = presence;
}
private async onSyncError(err: MatrixError): Promise<boolean> {
if (!this.running) {
debuglog("Sync no longer running: exiting");
+1 -2
View File
@@ -23,14 +23,13 @@ export class MediaTrackHandler {
const isNotNullAndKind = (track: MediaStreamTrack | null): boolean => {
return track !== null && track.kind === kind;
};
// @ts-ignore The linter don't get it
return this.pc
.getTransceivers()
.filter((t) => t.currentDirection === "sendonly" || t.currentDirection === "sendrecv")
.filter((t) => t.sender !== null)
.map((t) => t.sender)
.map((s) => s.track)
.filter(isNotNullAndKind);
.filter(isNotNullAndKind) as MediaStreamTrack[];
}
public getTackById(trackId: string): MediaStreamTrack | undefined {
+255 -840
View File
File diff suppressed because it is too large Load Diff