Compare commits

...

53 Commits

Author SHA1 Message Date
RiotRobot 47b729f085 v21.1.0 2022-11-08 14:31:37 +00:00
RiotRobot 05d980608a Prepare changelog for v21.1.0 2022-11-08 14:31:37 +00:00
RiotRobot 99af67a963 v21.1.0-rc.1 2022-11-01 14:38:04 +00:00
RiotRobot b77f5a5598 Prepare changelog for v21.1.0-rc.1 2022-11-01 14:38:04 +00:00
Michael Telatynski 76377c7cc4 Add eslint rule unicorn/no-instanceof-array (#2833) 2022-11-01 14:24:47 +00:00
RiotRobot 52830a2a50 Merge branch 'master' into develop
# Conflicts:
#	src/client.ts
2022-11-01 09:21:54 +00:00
RiotRobot 81c3668cb6 v21.0.1 2022-11-01 09:16:11 +00:00
RiotRobot 8f40dc6304 Prepare changelog for v21.0.1 2022-11-01 09:16:11 +00:00
Michael Telatynski fcdd8c93f4 [Backport staging] Catch server versions API call exception when starting the client (#2832)
Co-authored-by: Germain <germains@element.io>
2022-11-01 09:01:52 +00:00
ElementRobot 7d7803380c [Backport staging] Fix default behavior of Room.getBlacklistUnverifiedDevices (#2831)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-11-01 08:47:47 +00:00
Faye Duxovni 9fa6616052 Fix default behavior of Room.getBlacklistUnverifiedDevices (#2830) 2022-10-31 18:21:27 +00:00
Germain 1f3ae4bde2 Catch server versions API call exception when starting the client (#2828) 2022-10-31 17:44:52 +00:00
ElementRobot 545a74364d [Backport staging] Fix authedRequest including Authorization: Bearer undefined for password resets (#2829)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:14:06 +00:00
Michael Telatynski 646b3a69fe Fix authedRequest including Authorization: Bearer undefined for password resets (#2822) 2022-10-31 17:08:35 +00:00
ElementRobot db33f396b8 [Backport staging] Fix JSDoc (#2827)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:00:45 +00:00
Michael Telatynski 6c475d9b54 Fix JSDoc (#2825) 2022-10-31 16:50:15 +00:00
Janne Mareike Koschinski 068fbb7660 Loading threads with server-side assistance (#2735)
* Fix bug where undefined vs null in pagination tokens wasn't correctly handled
* Fix bug where thread list results were sorted incorrectly
* Allow removing the relationship of an event to a thread
* Implement feature detection for new threads MSCs and specs
* Prefix dir parameter for threads pagination if necessary
* Make threads conform to the same timeline APIs as any other timeline
* Extract thread timeline loading out of thread class
* fix thread roots not being updated correctly
* fix jumping to events by link
* implement new thread timeline loading
* Fix fetchRoomEvent incorrect return type

Co-authored-by: Germain <germains@element.io>
Co-authored-by: Germain <germain@souquet.com>
2022-10-28 13:48:14 +02:00
Michael Telatynski b44787192d Replace instanceof Array with Array.isArray (#2812) 2022-10-26 17:59:16 +01:00
Germain 6f2390a765 Switch ESLint warnings to be errors instead (#2814) 2022-10-26 16:25:40 +00:00
Germain dddc0aeccb Emit UnreadNotification event on notifications reset (#2804) 2022-10-26 14:23:54 +01:00
kegsay 9f6b42d3ae Merge pull request #2801 from matrix-org/kegan/ss-api-changes
Sliding sync: add include_old_rooms; remove is_tombstoned
2022-10-26 14:03:55 +01:00
Kegan Dougal 2e56c34df0 Merge branch 'develop' into kegan/ss-api-changes 2022-10-26 13:03:21 +01:00
Michael Telatynski 9f2f08dfd3 Fix more typescript --strict violations (#2795)
* Stash tsc fixes

* Iterate

* Iterate

* Iterate

* Fix tests

* Iterate

* Iterate

* Iterate

* Iterate

* Add tests
2022-10-25 18:31:40 +01:00
RiotRobot 4b3e6939d6 Resetting package fields for development 2022-10-25 17:05:31 +01:00
RiotRobot f2ae3bc8ef Merge branch 'master' into develop 2022-10-25 17:05:28 +01:00
Kegan Dougal 11cc30f345 Remove debug logging 2022-10-25 13:47:16 +01:00
Kegan Dougal 24a9562b07 bugfix: allow subtly different DELETE/INSERT semantics
In sliding sync, with an empty list, it is possible for the proxy
to send back DELETE 0, INSERT 0 !room which has the net result of
`[!room]`. Previously, the JS SDK would not handle this correctly.
Now it does. With tests.
2022-10-25 13:07:53 +01:00
Kegan Dougal 8f10c0d921 Sliding sync: add include_old_rooms; remove is_tombstoned 2022-10-25 10:45:38 +01:00
Michael Telatynski 9bdeea0a8d Fix incorrect prevEv being sent in ClientEvent.AccountData events (#2794) 2022-10-24 15:30:55 +01:00
Michael Telatynski ade2e81d3d Revert "Sliding sync: add include_old_rooms; remove is_tombstoned" (#2796) 2022-10-24 14:16:02 +01:00
kegsay 2fe434f3ae Merge pull request #2785 from matrix-org/kegan/ss-api-changes
Sliding sync: add include_old_rooms; remove is_tombstoned
2022-10-24 13:23:51 +01:00
Michael Telatynski 3a6561af36 Improve crypto init code and allow easier shimming (#2791) 2022-10-24 09:40:18 +01:00
renovate[bot] 403286cb81 Lock file maintenance (#2790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 09:07:13 +01:00
Kegan Dougal 219eab9139 Sliding sync: add include_old_rooms; remove is_tombstoned 2022-10-21 17:16:55 +01:00
Šimon Brandner a12e6185f9 Update call notification push rule to match MSC3914 (#2781) 2022-10-21 15:17:34 +00:00
Michael Telatynski d9eac57e9c Add room_type field to /publicRooms response (#2784) 2022-10-21 16:09:34 +01:00
renovate[bot] 9a9009d838 Update dependency jest-mock to v29 (#2775)
* Update dependency jest-mock to v29

* Update imports

* Strict fixes

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-21 14:22:50 +00:00
Michael Telatynski 6f729ad7fd Switch /keys/signatures/upload to v3 prefix (#2782) 2022-10-21 14:31:28 +01:00
Janne Mareike Koschinski cd33bafa04 fix build error caused by wrong ts-strict improvements (#2783)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-21 13:29:05 +00:00
Michael Telatynski 867a0ca7ee Apply more strict typescript around the codebase (#2778)
* Apply more strict typescript around the codebase

* Fix tests

* Revert strict mode commit

* Iterate strict

* Iterate

* Iterate strict

* Iterate

* Fix tests

* Iterate

* Iterate strict

* Add tests

* Iterate

* Iterate

* Fix tests

* Fix tests

* Strict types be strict

* Fix types

* detectOpenHandles

* Strict

* Fix client not stopping

* Add sync peeking tests

* Make test happier

* More strict

* Iterate

* Stabilise

* Moar strictness

* Improve coverage

* Fix types

* Fix types

* Improve types further

* Fix types

* Improve typing of NamespacedValue

* Fix types
2022-10-21 11:44:40 +01:00
Richard van der Hoff fdbbd9bca4 Merge pull request #2777 from matrix-org/rav/debug_to_device
An attempt to debug vector-im/element-web#23548: let's see if we can figure out where the to-device messages are getting lost.
2022-10-20 12:04:08 +01:00
Germain bf1137fc58 Fix to keep locally computed thread notifications (#2768) 2022-10-20 08:57:45 +01:00
renovate[bot] 508bb5841c Update all (#2774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 12:59:56 +00:00
Richard van der Hoff 35227e3a75 Merge pull request #2776 from matrix-org/rav/fix-readme 2022-10-19 11:47:10 +01:00
Richard van der Hoff 620a8d9c7f Add debugging for unsent to-device messages 2022-10-19 11:23:52 +01:00
renovate[bot] 17e16b9b1a Update jest monorepo to v29.2.0 (#2773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 10:55:47 +01:00
renovate[bot] 671dedca1c Update typescript-eslint monorepo to v5.40.1 (#2772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 10:54:36 +01:00
Hugh Nimmo-Smith 2464a691ef Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 (#2747)
* Clean implementation of MSC3886 and MSC3903

* Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better

* Start of some unit tests

* Make AES work on Node.js as well as browser

* Tests for ECDH/X25519

* stric mode linting

* Fix incorrect test

* Refactor full rendezvous logic out of react-sdk into js-sdk

* Use correct unstable import

* Pass fetch around

* Make correct usage of fetch in tests

* fix: you can't call fetch when it's not on window

* Use class names to make it clearer that these are unstable MSC implementations

* Linting

* Clean implementation of MSC3886 and MSC3903

* Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better

* Start of some unit tests

* Make AES work on Node.js as well as browser

* Tests for ECDH/X25519

* stric mode linting

* Fix incorrect test

* Refactor full rendezvous logic out of react-sdk into js-sdk

* Use correct unstable import

* Pass fetch around

* Make correct usage of fetch in tests

* fix: you can't call fetch when it's not on window

* Use class names to make it clearer that these are unstable MSC implementations

* Linting

* Reduce log noise

* Tidy up interface a bit

* Additional test for transport layer

* Linting

* Refactor dummy transport to be re-usable

* Remove redundant condition

* Handle more error cases

* Initial tests for MSC3906

* Reduce scope of PR to only cover generating a code on existing device

* Strict linting

* Additional test cases

* Lint

* additional test cases and remove some code smells

* More test cases

* Strict lint

* Strict lint

* Test case

* Refactor to handle UIA

* Unstable prefixes

* Lint

* Missed due to lack of strict...

* Test server capabilities using Feature

* Remove redundant assignment

* Refactor ro resuse generateDecimal from SAS

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/channels/ecdhV1.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Rename files to titlecase

* Visibility modifiers

* Resolve public mutability

* Refactor logic to reduce duplication

* Refactor to have better defined data types throughout

* Rebase and remove Node.js crypto

* Wipe AES key out after use

* Add typing for MSC3906 layer

* Strict lint

* Fix double connect detection

* Remove unintended debug statement

* Return types

* Use generics

* Make type of MSC3903ECDHPayload explicit

* Use unstable prefix for RendezvousChannelAlgorithm

* Fix

* Extra unstable type

* Test types

Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Kerry <kerrya@element.io>
2022-10-19 09:30:15 +00:00
Richard van der Hoff d548b04d06 README.md: fix jsdoc viewer incantation
SimpleHTTPServer was python 2.
2022-10-19 10:19:23 +01:00
Richard van der Hoff 7ffdf17213 Merge pull request #2767 from matrix-org/rav/fix_comment 2022-10-19 10:13:18 +01:00
Valere 1c3dd0e51e Encryption should not hinder verification (#2734)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-10-18 15:56:34 -04:00
Michael Telatynski 0231d40277 Fix POST data not being passed for registerWithIdentityServer (#2769) 2022-10-18 15:58:17 +01:00
Richard van der Hoff b1e9f39d65 Remove incorrect comment
As with most of the crypto functionality, `setCryptoTrustCrossSignedDevices`
cannot be called until after `initCrypto`.
2022-10-17 22:58:04 +01:00
158 changed files with 7301 additions and 3739 deletions
+7 -7
View File
@@ -20,13 +20,13 @@ module.exports = {
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
"no-var": ["warn"],
"prefer-rest-params": ["warn"],
"prefer-spread": ["warn"],
"one-var": ["warn"],
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
"no-var": ["error"],
"prefer-rest-params": ["error"],
"prefer-spread": ["error"],
"one-var": ["error"],
"padded-blocks": ["error"],
"no-extend-native": ["error"],
"camelcase": ["error"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
+22
View File
@@ -1,3 +1,25 @@
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
==================================================================================================
## ✨ Features
* Loading threads with server-side assistance ([\#2735](https://github.com/matrix-org/matrix-js-sdk/pull/2735)). Contributed by @justjanne.
* Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 ([\#2747](https://github.com/matrix-org/matrix-js-sdk/pull/2747)). Contributed by @hughns.
## 🐛 Bug Fixes
* Replace `instanceof Array` with `Array.isArray` ([\#2812](https://github.com/matrix-org/matrix-js-sdk/pull/2812)). Fixes #2811.
* Emit UnreadNotification event on notifications reset ([\#2804](https://github.com/matrix-org/matrix-js-sdk/pull/2804)). Fixes vector-im/element-web#23590.
* Fix incorrect prevEv being sent in ClientEvent.AccountData events ([\#2794](https://github.com/matrix-org/matrix-js-sdk/pull/2794)).
* Fix build error caused by wrong ts-strict improvements ([\#2783](https://github.com/matrix-org/matrix-js-sdk/pull/2783)). Contributed by @justjanne.
* Encryption should not hinder verification ([\#2734](https://github.com/matrix-org/matrix-js-sdk/pull/2734)).
Changes in [21.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.1) (2022-11-01)
==================================================================================================
## 🐛 Bug Fixes
* Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni.
* Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634.
* Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655.
Changes in [21.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.0) (2022-10-25)
==================================================================================================
+1 -1
View File
@@ -301,7 +301,7 @@ host the API reference from the source files like this:
```
$ yarn gendoc
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
$ python -m http.server 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "21.0.0",
"version": "21.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -91,16 +91,17 @@
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"domexception": "^4.0.0",
"eslint": "8.24.0",
"eslint": "8.25.0",
"eslint-config-google": "^0.14.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.6.0",
"eslint-plugin-matrix-org": "^0.7.0",
"eslint-plugin-unicorn": "^44.0.2",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^27.5.1",
"jest-mock": "^29.0.0",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^2.5.0",
+13 -13
View File
@@ -38,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
export class TestClient {
public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient;
public deviceKeys: IDeviceKeys;
public oneTimeKeys: Record<string, IOneTimeKey>;
public deviceKeys?: IDeviceKeys | null;
public oneTimeKeys?: Record<string, IOneTimeKey>;
constructor(
public readonly userId?: string,
@@ -123,7 +123,7 @@ export class TestClient {
logger.log(this + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys).length).toEqual(0);
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
@@ -138,9 +138,9 @@ export class TestClient {
* @returns {Promise} for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys).length != 0) {
if (Object.keys(this.oneTimeKeys!).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
return Promise.resolve(this.oneTimeKeys!);
}
this.httpBackend.when("POST", "/keys/upload")
@@ -148,7 +148,7 @@ export class TestClient {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
});
@@ -158,17 +158,17 @@ export class TestClient {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
Object.keys(content.one_time_keys!).length);
this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
return this.oneTimeKeys!;
});
}
@@ -183,7 +183,7 @@ export class TestClient {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual([]);
expect(content.device_keys![userId]).toEqual([]);
});
return response;
});
@@ -206,7 +206,7 @@ export class TestClient {
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
return this.deviceKeys!.keys[keyId];
}
/**
@@ -216,7 +216,7 @@ export class TestClient {
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
return this.deviceKeys!.keys[keyId];
}
/**
@@ -237,6 +237,6 @@ export class TestClient {
}
public getUserId(): string {
return this.userId;
return this.userId!;
}
}
+6 -6
View File
@@ -59,7 +59,7 @@ async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}
/**
@@ -99,7 +99,7 @@ async function expectAliClaimKeys(): Promise<void> {
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
@@ -137,7 +137,7 @@ async function aliDownloadsKeys(): Promise<void> {
// @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified).
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
@@ -223,7 +223,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
@@ -393,7 +393,7 @@ describe("MatrixClient crypto", () => {
it("Ali gets keys with an invalid signature", async () => {
await bobUploadsDeviceKeys();
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys;
const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([
@@ -479,7 +479,7 @@ describe("MatrixClient crypto", () => {
await bobTestClient.start();
const keys = await bobTestClient.awaitOneTimeKeyUpload();
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
});
it("Ali sends a message", async () => {
+151 -85
View File
@@ -342,8 +342,14 @@ describe("MatrixClient event timelines", function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(FeatureSupport.None);
Thread.setServerSideListSupport(FeatureSupport.None);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
});
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
}
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId)!;
@@ -595,22 +601,8 @@ describe("MatrixClient event timelines", function() {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
client.stopClient(); // we don't need the client to be syncing at this time
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
const timelineSet = thread.timelineSet;
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
@@ -619,7 +611,7 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
@@ -628,9 +620,45 @@ describe("MatrixClient event timelines", function() {
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const timeline = await timelinePromise;
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const timeline = await timelinePromise;
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
@@ -1025,10 +1053,6 @@ describe("MatrixClient event timelines", function() {
});
describe("paginateEventTimeline for thread list timeline", function() {
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
}
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
function respondToFilter(): ExpectedHttpRequest {
@@ -1047,10 +1071,10 @@ describe("MatrixClient event timelines", function() {
response = {
chunk: [THREAD_ROOT],
state: [],
next_batch: RANDOM_TOKEN,
next_batch: RANDOM_TOKEN as string | null,
},
): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/v1/rooms/$roomId/threads", {
$roomId: roomId,
}));
request.respond(200, response);
@@ -1089,8 +1113,9 @@ describe("MatrixClient event timelines", function() {
beforeEach(() => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
});
async function testPagination(timelineSet: EventTimelineSet, direction: Direction) {
@@ -1111,7 +1136,7 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate all threads backwards", async function() {
const room = client.getRoom(roomId);
const timelineSets = await (room?.createThreadsTimelineSets());
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
const [allThreads, myThreads] = timelineSets!;
await testPagination(allThreads, Direction.Backward);
@@ -1120,7 +1145,7 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate all threads forwards", async function() {
const room = client.getRoom(roomId);
const timelineSets = await (room?.createThreadsTimelineSets());
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
const [allThreads, myThreads] = timelineSets!;
@@ -1130,7 +1155,7 @@ describe("MatrixClient event timelines", function() {
it("should allow fetching all threads", async function() {
const room = client.getRoom(roomId)!;
const timelineSets = await room.createThreadsTimelineSets();
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
respondToThreads();
respondToThreads();
@@ -1418,74 +1443,115 @@ describe("MatrixClient event timelines", function() {
});
});
it("should re-insert room IDs for bundled thread relation events", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_ROOT,
],
prev_batch: "f_1_1",
describe("should re-insert room IDs for bundled thread relation events", () => {
async function doTest() {
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_ROOT,
],
prev_batch: "f_1_1",
},
},
},
},
},
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
const room = client.getRoom(roomId)!;
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const timelineSet = thread.timelineSet;
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});
await Promise.all([
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
httpBackend.flushAllExpected(),
]);
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_REPLY,
],
prev_batch: "f_1_2",
const room = client.getRoom(roomId)!;
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const timelineSet = thread.timelineSet;
const buildParams = (direction: Direction, token: string): string => {
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
return `?from=${token}&org.matrix.msc3715.dir=${direction}`;
} else {
return `?dir=${direction}&from=${token}`;
}
};
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
};
});
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_REPLY,
],
prev_batch: "f_1_2",
},
},
},
},
},
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
}
it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
return doTest();
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
it("in backwards compatible unstable mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.Experimental);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
return doTest();
});
it("in backwards compatible mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.None);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
return doTest();
});
});
});
+53 -29
View File
@@ -139,7 +139,7 @@ describe("MatrixClient", function() {
const r = client!.cancelUpload(prom);
expect(r).toBe(true);
await expect(prom).rejects.toThrow("Aborted");
expect(client.getCurrentUploads()).toHaveLength(0);
expect(client!.getCurrentUploads()).toHaveLength(0);
});
});
@@ -178,7 +178,7 @@ describe("MatrixClient", function() {
expect(request.data.third_party_signed).toEqual(signature);
}).respond(200, { room_id: roomId });
const prom = client.joinRoom(roomId, {
const prom = client!.joinRoom(roomId, {
inviteSignUrl,
viaServers,
});
@@ -658,7 +658,7 @@ describe("MatrixClient", function() {
// The vote event has been copied into the thread
const eventRefWithThreadId = withThreadId(
eventPollResponseReference, eventPollStartThreadRoot.getId());
eventPollResponseReference, eventPollStartThreadRoot.getId()!);
expect(eventRefWithThreadId.threadRootId).toBeTruthy();
expect(threaded).toEqual([
@@ -695,7 +695,7 @@ describe("MatrixClient", function() {
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
]);
});
@@ -725,7 +725,7 @@ describe("MatrixClient", function() {
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
eventMessageInThread,
]);
});
@@ -757,7 +757,7 @@ describe("MatrixClient", function() {
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
withThreadId(eventReaction, eventPollStartThreadRoot.getId()!),
]);
});
@@ -813,7 +813,7 @@ describe("MatrixClient", function() {
// Thread should contain only stuff that happened in the thread - no room state events
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!),
eventMessageInThread,
]);
});
@@ -1164,18 +1164,18 @@ describe("MatrixClient", function() {
describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {});
httpBackend!.when("POST", "/logout").respond(200, {});
const fn = jest.fn();
client.http.request(Method.Get, "/test").catch(fn);
client.logout(true);
await httpBackend.flush(undefined);
client!.http.request(Method.Get, "/test").catch(fn);
client!.logout(true);
await httpBackend!.flush(undefined);
expect(fn).toHaveBeenCalled();
});
});
describe("sendHtmlEmote", () => {
it("should send valid html emote", async () => {
httpBackend.when("PUT", "/send").check(req => {
httpBackend!.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({
"msgtype": "m.emote",
"body": "Body",
@@ -1184,15 +1184,15 @@ describe("MatrixClient", function() {
"org.matrix.msc1767.message": expect.anything(),
});
}).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined);
const prom = client!.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
await httpBackend!.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
});
});
describe("sendHtmlMessage", () => {
it("should send valid html message", async () => {
httpBackend.when("PUT", "/send").check(req => {
httpBackend!.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({
"msgtype": "m.text",
"body": "Body",
@@ -1201,24 +1201,24 @@ describe("MatrixClient", function() {
"org.matrix.msc1767.message": expect.anything(),
});
}).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined);
const prom = client!.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
await httpBackend!.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
});
});
describe("forget", () => {
it("should remove from store by default", async () => {
const room = new Room("!roomId:server", client, userId);
client.store.storeRoom(room);
expect(client.store.getRooms()).toContain(room);
const room = new Room("!roomId:server", client!, userId);
client!.store.storeRoom(room);
expect(client!.store.getRooms()).toContain(room);
httpBackend.when("POST", "/forget").respond(200, {});
httpBackend!.when("POST", "/forget").respond(200, {});
await Promise.all([
client.forget(room.roomId),
httpBackend.flushAllExpected(),
client!.forget(room.roomId),
httpBackend!.flushAllExpected(),
]);
expect(client.store.getRooms()).not.toContain(room);
expect(client!.store.getRooms()).not.toContain(room);
});
});
@@ -1306,8 +1306,32 @@ describe("MatrixClient", function() {
const resp = await prom;
expect(resp.access_token).toBe(token);
expect(resp.user_id).toBe(userId);
expect(client.getUserId()).toBe(userId);
expect(client.http.opts.accessToken).toBe(token);
expect(client!.getUserId()).toBe(userId);
expect(client!.http.opts.accessToken).toBe(token);
});
});
describe("registerWithIdentityServer", () => {
it("should pass data to POST request", async () => {
const token = {
access_token: "access_token",
token_type: "Bearer",
matrix_server_name: "server_name",
expires_in: 12345,
};
httpBackend!.when("POST", "/account/register").check(req => {
expect(req.data).toStrictEqual(token);
}).respond(200, {
access_token: "at",
token: "tt",
});
const prom = client!.registerWithIdentityServer(token);
await httpBackend!.flushAllExpected();
const resp = await prom;
expect(resp.access_token).toBe("at");
expect(resp.token).toBe("tt");
});
});
@@ -1351,7 +1375,7 @@ const buildEventMessageInThread = (root: MatrixEvent) => new MatrixEvent({
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
"event_id": root.getId()!,
},
"rel_type": "m.thread",
},
@@ -1450,13 +1474,13 @@ const buildEventReply = (target: MatrixEvent) => new MatrixEvent({
"device_id": "XISFUZSKHH",
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
"event_id": target.getId()!,
},
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": target.getId() + Math.random(),
"event_id": target.getId()! + Math.random(),
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
+4 -4
View File
@@ -60,7 +60,7 @@ describe("MatrixClient relations", () => {
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with relation type", async () => {
@@ -72,7 +72,7 @@ describe("MatrixClient relations", () => {
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with relation type and event type", async () => {
@@ -87,7 +87,7 @@ describe("MatrixClient relations", () => {
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with custom options", async () => {
@@ -107,7 +107,7 @@ describe("MatrixClient relations", () => {
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it('should use default direction in the fetchRelations endpoint', async () => {
+111
View File
@@ -274,6 +274,16 @@ describe("MatrixClient syncing", () => {
expect(fires).toBe(1);
});
it("should work when all network calls fail", async () => {
httpBackend!.expectedRequests = [];
httpBackend!.when("GET", "").fail(0, new Error("CORS or something"));
const prom = client!.startClient();
await Promise.all([
expect(prom).resolves.toBeUndefined(),
httpBackend!.flushAllExpected(),
]);
});
});
describe("initial sync", () => {
@@ -1541,6 +1551,107 @@ describe("MatrixClient syncing", () => {
});
});
describe("peek", () => {
beforeEach(() => {
httpBackend!.expectedRequests = [];
});
it("should return a room based on the room initialSync API", async () => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: "leave",
messages: {
start: "start",
end: "end",
chunk: [{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
}, {
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
room_id: roomOne,
}],
},
state: [{
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
}],
presence: [{
content: {},
type: "m.presence",
sender: userA,
}],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
const prom = client!.peekInRoom(roomOne);
await httpBackend!.flushAllExpected();
const room = await prom;
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe("leave");
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
});
});
describe("user account data", () => {
it("should include correct prevEv in the ClientEvent.AccountData emit", async () => {
const eventA1 = new MatrixEvent({ type: "a", content: { body: "1" } });
const eventA2 = new MatrixEvent({ type: "a", content: { body: "2" } });
const eventB1 = new MatrixEvent({ type: "b", content: { body: "1" } });
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
client!.store.storeAccountDataEvents([eventA1, eventB1]);
const fn = jest.fn();
client!.on(ClientEvent.AccountData, fn);
httpBackend!.when("GET", "/sync").respond(200, {
next_batch: "batch_token",
rooms: {},
presence: {},
account_data: {
events: [eventA2.event, eventB2.event],
},
});
await Promise.all([
client!.startClient(),
httpBackend!.flushAllExpected(),
]);
const eventA = client?.getAccountData("a");
expect(eventA).not.toBe(eventA1);
const eventB = client?.getAccountData("b");
expect(eventB).not.toBe(eventB1);
expect(fn).toHaveBeenCalledWith(eventA, eventA1);
expect(fn).toHaveBeenCalledWith(eventB, eventB1);
expect(eventA?.getContent().body).toBe("2");
expect(eventB?.getContent().body).toBe("2");
client!.off(ClientEvent.AccountData, fn);
});
});
/**
* waits for the MatrixClient to emit one or more 'sync' events.
*
+8 -8
View File
@@ -1160,11 +1160,11 @@ describe("megolm", () => {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"session_key": groupSessionKey!.key,
"chain_index": groupSessionKey!.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
@@ -1298,11 +1298,11 @@ describe("megolm", () => {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"session_key": groupSessionKey!.key,
"chain_index": groupSessionKey!.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
+1 -2
View File
@@ -468,7 +468,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
{ pos: "h", lists: [], rooms: {}, extensions: {} },
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
@@ -490,7 +490,6 @@ describe("SlidingSyncSdk", () => {
SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} },
null,
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
});
+101 -16
View File
@@ -82,7 +82,7 @@ describe("SlidingSync", () => {
it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
const roomId = "!sub:localhost";
const subInfo = {
timeline_limit: 42,
@@ -108,7 +108,7 @@ describe("SlidingSync", () => {
// expect everything to be sent
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
@@ -117,7 +117,7 @@ describe("SlidingSync", () => {
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined();
expect(req.queryParams!["pos"]).toBeUndefined();
txnId = body.txn_id;
}).respond(200, function() {
return {
@@ -127,10 +127,10 @@ describe("SlidingSync", () => {
txn_id: txnId,
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
// expect nothing but ranges and non-initial extensions to be sent
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
@@ -139,7 +139,7 @@ describe("SlidingSync", () => {
});
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
expect(req.queryParams["pos"]).toEqual("11");
expect(req.queryParams!["pos"]).toEqual("11");
}).respond(200, function() {
return {
pos: "12",
@@ -147,19 +147,19 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
// now we expire the session
httpBackend.when("POST", syncUrl).respond(400, function() {
httpBackend!.when("POST", syncUrl).respond(400, function() {
logger.debug("sending session expired 400");
return {
error: "HTTP 400 : session expired",
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
// ...and everything should be sent again
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
@@ -168,7 +168,7 @@ describe("SlidingSync", () => {
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined();
expect(req.queryParams!["pos"]).toBeUndefined();
}).respond(200, function() {
return {
pos: "1",
@@ -176,7 +176,7 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
slidingSync.stop();
});
});
@@ -415,7 +415,7 @@ describe("SlidingSync", () => {
expect(slidingSync.getList(0)).toBeDefined();
expect(slidingSync.getList(5)).toBeNull();
expect(slidingSync.getListData(5)).toBeNull();
const syncData = slidingSync.getListData(0);
const syncData = slidingSync.getListData(0)!;
expect(syncData.joinedCount).toEqual(500); // from previous test
expect(syncData.roomIndexToRoomId).toEqual({
0: roomA,
@@ -665,7 +665,7 @@ describe("SlidingSync", () => {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
@@ -703,7 +703,7 @@ describe("SlidingSync", () => {
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
@@ -739,7 +739,7 @@ describe("SlidingSync", () => {
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend!.when("POST", syncUrl).respond(200, {
@@ -806,6 +806,91 @@ describe("SlidingSync", () => {
await listPromise;
slidingSync.stop();
});
// Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't
// end up losing room IDs.
it("should handle insertions with a spurious DELETE correctly", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [
{
ranges: [[0, 20]],
},
], {}, client!, 1);
// initially start with nothing
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "a",
lists: [{
count: 0,
ops: [],
}],
});
slidingSync.start();
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
// insert a room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "b",
lists: [{
count: 1,
ops: [
{
op: "DELETE", index: 0,
},
{
op: "INSERT", index: 0, room_id: roomA,
},
],
}],
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomA,
});
// insert another room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: [{
count: 1,
ops: [
{
op: "DELETE", index: 1,
},
{
op: "INSERT", index: 0, room_id: roomB,
},
],
}],
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomA,
});
// insert a final room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: [{
count: 1,
ops: [
{
op: "DELETE", index: 2,
},
{
op: "INSERT", index: 0, room_id: roomC,
},
],
}],
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
slidingSync.stop();
});
});
describe("transaction IDs", () => {
+4 -4
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MethodKeysOf, mocked, MockedObject } from "jest-mock";
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
@@ -26,7 +26,7 @@ import { User } from "../../src/models/user";
* to MatrixClient events
*/
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
constructor(mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> = {}) {
super();
Object.assign(this, mockProperties);
}
@@ -44,7 +44,7 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
@@ -84,7 +84,7 @@ export const mockClientMethodsEvents = () => ({
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
+1 -1
View File
@@ -307,7 +307,7 @@ export function mkReplyMessage(
"rel_type": "m.in_reply_to",
"event_id": opts.replyToMessage.getId(),
"m.in_reply_to": {
"event_id": opts.replyToMessage.getId(),
"event_id": opts.replyToMessage.getId()!,
},
},
},
+1 -1
View File
@@ -135,7 +135,7 @@ export class MockMediaDeviceInfo {
export class MockMediaHandler {
getUserMediaStream(audio: boolean, video: boolean) {
const tracks = [];
const tracks: MockMediaStreamTrack[] = [];
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
+8 -8
View File
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null);
const ns = new NamespacedValue("stable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
@@ -41,17 +41,17 @@ describe("NamespacedValue", () => {
it("should match against either stable or unstable", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.matches("no")).toBe(false);
expect(ns.matches(ns.stable)).toBe(true);
expect(ns.matches(ns.unstable)).toBe(true);
expect(ns.matches(ns.stable!)).toBe(true);
expect(ns.matches(ns.unstable!)).toBe(true);
});
it("should not permit falsey values for both parts", () => {
try {
new UnstableValue(null, null);
new UnstableValue(null!, null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("One of stable or unstable values must be supplied");
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
}
});
});
@@ -65,7 +65,7 @@ describe("UnstableValue", () => {
});
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable");
const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
@@ -73,11 +73,11 @@ describe("UnstableValue", () => {
it("should not permit falsey unstable values", () => {
try {
new UnstableValue("stable", null);
new UnstableValue("stable", null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("Unstable value must be supplied");
expect((<Error>e).message).toBe("Unstable value must be supplied");
}
});
});
+1 -1
View File
@@ -678,7 +678,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
+16
View File
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() {
+122 -75
View File
@@ -2,6 +2,7 @@ import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { MatrixClient } from "../../src/client";
import { Crypto } from "../../src/crypto";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
@@ -32,7 +33,7 @@ function awaitEvent(emitter, event) {
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> {
const roomId = event.getRoomId();
const eventContent = event.getWireContent();
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
const key = await client.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
eventContent.sender_key,
eventContent.session_id,
@@ -68,10 +69,10 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
const roomId = event.getRoomId();
const eventContent = event.getWireContent();
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
const ksEvent = new MatrixEvent({
type: "m.room_key",
sender: client.getUserId(),
sender: client.getUserId()!,
content: {
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
@@ -146,7 +147,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
@@ -334,7 +335,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -343,17 +344,17 @@ describe("Crypto", function() {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -365,14 +366,14 @@ describe("Crypto", function() {
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient.crypto.cryptoStore;
const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
@@ -437,7 +438,7 @@ describe("Crypto", function() {
});
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private property
event.clearEvent = undefined;
@@ -446,24 +447,24 @@ describe("Crypto", function() {
// @ts-ignore private property
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
ksEvent.getContent().sender_key = undefined; // test
bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn();
bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn();
await bobDecryptor.onRoomKeyEvent(ksEvent);
expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled();
});
it("creates a new keyshare request if we request a keyshare", async function() {
@@ -479,7 +480,7 @@ describe("Crypto", function() {
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient.crypto.cryptoStore;
const cryptoStore = aliceClient.crypto!.cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
@@ -514,7 +515,7 @@ describe("Crypto", function() {
// let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now.
// @ts-ignore
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceSendToDevice).toBeCalledTimes(1);
@@ -571,7 +572,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -580,18 +581,18 @@ describe("Crypto", function() {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto.cryptoStore;
const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
@@ -604,11 +605,11 @@ describe("Crypto", function() {
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest(
outgoingReq.requestId, RoomKeyRequestState.Unsent,
outgoingReq!.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent },
);
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -617,7 +618,7 @@ describe("Crypto", function() {
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -675,7 +676,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -684,18 +685,18 @@ describe("Crypto", function() {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -703,10 +704,10 @@ describe("Crypto", function() {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
ksEvent.event.sender = claraClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -753,7 +754,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -762,19 +763,19 @@ describe("Crypto", function() {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
const device = new DeviceInfo(claraClient.deviceId!);
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -782,10 +783,10 @@ describe("Crypto", function() {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
ksEvent.event.sender = bobClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -835,7 +836,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -844,26 +845,26 @@ describe("Crypto", function() {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const device = new DeviceInfo(claraClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
ksEvent.event.sender = claraClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -904,7 +905,7 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -914,11 +915,11 @@ describe("Crypto", function() {
event.claimedEd25519Key = null;
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -926,25 +927,25 @@ describe("Crypto", function() {
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
expect(bobKey).toBeNull();
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId);
expect(parked).toEqual([{
senderId: aliceClient.getUserId(),
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: aliceKey.key,
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
sessionKey: aliceKey!.key,
keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key },
forwardingCurve25519KeyChain: ["akey"],
}]);
});
@@ -956,19 +957,19 @@ describe("Crypto", function() {
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.uploadKeySignatures = jest.fn();
client.crypto.baseApis.http.authedRequest = jest.fn();
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto!.isCrossSigningReady = async () => false;
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.uploadKeySignatures = jest.fn();
client.crypto!.baseApis.http.authedRequest = jest.fn();
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
await client.crypto!.bootstrapSecretStorage({
createSecretStorageKey,
});
client.stopClient();
@@ -995,7 +996,7 @@ describe("Crypto", function() {
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto.olmDevice.deviceCurve25519Key,
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: JSON.stringify(payload) },
};
});
@@ -1075,4 +1076,50 @@ describe("Crypto", function() {
client.httpBackend.verifyNoOutstandingRequests();
});
});
describe("checkSecretStoragePrivateKey", () => {
let client: TestClient;
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
});
afterEach(async () => {
await client.stop();
});
it("should free PkDecryption", () => {
const free = jest.fn();
jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({
init_with_private_key: jest.fn(),
free,
}) as unknown as PkDecryption);
client.client.checkSecretStoragePrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled();
});
});
describe("checkCrossSigningPrivateKey", () => {
let client: TestClient;
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
});
afterEach(async () => {
await client.stop();
});
it("should free PkSigning", () => {
const free = jest.fn();
jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({
init_with_seed: jest.fn(),
free,
}) as unknown as PkSigning);
client.client.checkCrossSigningPrivateKey(new Uint8Array(), "");
expect(free).toHaveBeenCalled();
});
});
});
+6 -6
View File
@@ -222,9 +222,9 @@ describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
() => new IndexedDBCryptoStore(undefined!, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
@@ -247,14 +247,14 @@ describe.each([
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice);
await storeCrossSigningKeyCache("self_signing", testKey);
await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", "");
const nokey = await getCrossSigningKeyCache!("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", "");
expect(new Uint8Array(key)).toEqual(testKey);
const key = await getCrossSigningKeyCache!("self_signing", "");
expect(new Uint8Array(key!)).toEqual(testKey);
});
});
+1 -1
View File
@@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
let deviceLists = [];
let deviceLists: DeviceList[] = [];
beforeEach(function() {
deviceLists = [];
+129 -33
View File
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const ROOM_ID = '!ROOM:ID';
@@ -331,7 +331,7 @@ describe("MegolmDecryption", function() {
},
},
});
mockBaseApis.sendToDevice.mockResolvedValue(undefined);
mockBaseApis.sendToDevice.mockResolvedValue({});
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
aliceDeviceInfo = {
@@ -493,9 +493,9 @@ describe("MegolmDecryption", function() {
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice1 = bobClient1.crypto.olmDevice;
const bobDevice2 = bobClient2.crypto.olmDevice;
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice1 = bobClient1.crypto!.olmDevice;
const bobDevice2 = bobClient2.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -515,8 +515,8 @@ describe("MegolmDecryption", function() {
bobdevice1: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice1.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key!,
},
verified: 0,
known: false,
@@ -524,18 +524,19 @@ describe("MegolmDecryption", function() {
bobdevice2: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice2.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key!,
},
verified: -1,
known: false,
},
};
aliceClient.crypto.deviceList.storeDevicesForUser(
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore short-circuiting private method
return this.getDevicesFromStore(userIds);
};
@@ -551,7 +552,7 @@ describe("MegolmDecryption", function() {
body: "secret",
},
});
await aliceClient.crypto.encryptEvent(event, room);
await aliceClient.crypto!.encryptEvent(event, room);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
@@ -583,6 +584,100 @@ describe("MegolmDecryption", function() {
bobClient2.stopClient();
});
it("does not block unverified devices when sending verification events", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobMember = new RoomMember(roomId, "@bob:example.com");
room.getEncryptionTargetMembers = async function() {
return [bobMember];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES: Record<string, IDevice> = {
bobdevice: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
},
verified: 0,
known: true,
},
};
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore private
return this.getDevicesFromStore(userIds);
};
await bobDevice.generateOneTimeKeys(1);
const oneTimeKeys = await bobDevice.getOneTimeKeys();
const signedOneTimeKeys: Record<string, { key: string, signatures: object }> = {};
for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
const k = {
key: oneTimeKeys.curve25519[keyId],
signatures: {},
};
signedOneTimeKeys["signed_curve25519:" + keyId] = k;
await bobClient.crypto!.signObject(k);
break;
}
}
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
one_time_keys: {
'@bob:example.com': {
bobdevice: signedOneTimeKeys,
},
},
failures: {},
});
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.key.verification.start",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {
from_device: "alicedevice",
method: "m.sas.v1",
transaction_id: "transactionid",
},
});
await aliceClient.crypto!.encryptEvent(event, room);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toEqual("m.room.encrypted");
aliceClient.stopClient();
bobClient.stopClient();
});
it("notifies devices when unable to create olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
@@ -594,8 +689,8 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice = bobClient.crypto.olmDevice;
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -624,18 +719,19 @@ describe("MegolmDecryption", function() {
device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
},
known: true,
verified: 1,
},
};
aliceClient.crypto.deviceList.storeDevicesForUser(
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore private
return this.getDevicesFromStore(userIds);
};
@@ -654,7 +750,7 @@ describe("MegolmDecryption", function() {
event_id: "$event",
content: {},
});
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
@@ -685,10 +781,10 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient.crypto.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
const roomId = "!someroom";
@@ -705,7 +801,7 @@ describe("MegolmDecryption", function() {
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -732,7 +828,7 @@ describe("MegolmDecryption", function() {
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -762,10 +858,10 @@ describe("MegolmDecryption", function() {
]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto.downloadKeys = jest.fn();
const bobDevice = bobClient.crypto.olmDevice;
aliceClient.crypto!.downloadKeys = jest.fn();
const bobDevice = bobClient.crypto!.olmDevice;
const roomId = "!someroom";
@@ -788,7 +884,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -820,7 +916,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -850,10 +946,10 @@ describe("MegolmDecryption", function() {
bobClient.initCrypto(),
]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
const bobDevice = bobClient.crypto.olmDevice;
aliceClient.crypto.downloadKeys = jest.fn();
const bobDevice = bobClient.crypto!.olmDevice;
aliceClient.crypto!.downloadKeys = jest.fn();
const roomId = "!someroom";
@@ -875,7 +971,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
+6 -6
View File
@@ -67,13 +67,13 @@ describe("OlmDevice", function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
@@ -94,7 +94,7 @@ describe("OlmDevice", function() {
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE,
) as any; // OlmDevice.encryptMessage has incorrect return type
@@ -103,7 +103,7 @@ describe("OlmDevice", function() {
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
@@ -118,7 +118,7 @@ describe("OlmDevice", function() {
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE_2,
) as any; // OlmDevice.encryptMessage has incorrect return type
@@ -128,7 +128,7 @@ describe("OlmDevice", function() {
// Note: "decrypted_2" does not have the same structure as "decrypted"
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
decrypted.session_id,
ciphertext2.type,
ciphertext2.body,
+33 -7
View File
@@ -34,7 +34,7 @@ import { MatrixScheduler } from '../../../src';
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const ROOM_ID = '!ROOM:ID';
@@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f!, n/100);
return realSetTimeout(f!, n!/100);
});
});
@@ -318,7 +318,7 @@ describe("MegolmBackup", function() {
resolve();
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -349,7 +349,7 @@ describe("MegolmBackup", function() {
return client.initCrypto()
.then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
@@ -401,7 +401,7 @@ describe("MegolmBackup", function() {
resolve();
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -449,7 +449,7 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
);
} catch (e) {
reject(e);
@@ -568,7 +568,7 @@ describe("MegolmBackup", function() {
);
}
};
return client.crypto.backupManager.backupGroupSession(
return client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -699,4 +699,30 @@ describe("MegolmBackup", function() {
)).rejects.toThrow();
});
});
describe("flagAllGroupSessionsForBackup", () => {
it("should return number of sesions needing backup", async () => {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
fetchFn: jest.fn(), // NOP
store,
scheduler,
userId: "@alice:bar",
deviceId: "device",
cryptoStore,
});
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
client.stopClient();
});
});
});
+90 -36
View File
@@ -93,8 +93,8 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
await olmlib.verifySignature(
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
alice.crypto!.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
);
});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -152,7 +152,7 @@ describe("Cross Signing", function() {
authUploadDeviceSigningKeys,
});
} catch (e) {
if (e.errcode === "M_FORBIDDEN") {
if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
bootstrapDidThrow = true;
}
}
@@ -169,7 +169,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -238,12 +238,12 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
try {
await olmlib.verifySignature(
alice.crypto.olmDevice,
alice.crypto!.olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
@@ -258,7 +258,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -266,7 +266,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
await alice.crypto.signObject(aliceDevice);
await alice.crypto!.signObject(aliceDevice);
olmlib.pkSign(
aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning,
@@ -401,7 +401,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -435,7 +435,7 @@ describe("Cross Signing", function() {
verified: 0,
known: false,
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be TOFU
@@ -467,11 +467,11 @@ describe("Cross Signing", function() {
const aliceKeys: Record<string, PkSigning> = {};
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
null,
undefined,
aliceKeys,
);
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -486,7 +486,7 @@ describe("Cross Signing", function() {
]);
const keyChangePromise = new Promise<void>((resolve, reject) => {
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
@@ -494,7 +494,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -502,7 +502,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
await alice.crypto.signObject(aliceDevice);
await alice.crypto!.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
@@ -667,7 +667,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -690,7 +690,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice as unknown as IDevice,
});
// Bob's device key should be untrusted
@@ -735,7 +735,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -770,7 +770,7 @@ describe("Cross Signing", function() {
},
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
@@ -802,7 +802,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -838,8 +838,8 @@ describe("Cross Signing", function() {
// Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
@@ -876,20 +876,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => ({ failures: {} });
// set Bob's cross-signing key
await resetCrossSigningKeys(bob);
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
"curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!,
"ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!,
},
verified: 1,
known: true,
},
});
alice.crypto.deviceList.storeCrossSigningForUser(
alice.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob.crypto.crossSigningInfo.toStorage(),
bob.crypto!.crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => ({});
@@ -909,8 +909,8 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"];
delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures!["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
@@ -919,9 +919,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
await new Promise((resolve) => {
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve);
});
await upgradePromise;
@@ -963,7 +963,7 @@ describe("Cross Signing", function() {
};
// Alice's device downloads the keys, but doesn't trust them yet
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -999,7 +999,7 @@ describe("Cross Signing", function() {
["ed25519:" + alicePubkey]: sig,
},
} };
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[aliceDeviceId]: aliceCrossSignedDevice,
});
@@ -1042,7 +1042,7 @@ describe("Cross Signing", function() {
};
// Alice's device downloads the keys
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -1067,11 +1067,65 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[deviceId]: aliceNotCrossSignedDevice,
});
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
alice.stopClient();
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + alicePubkey]: alicePubkey,
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
},
};
// Alice's device downloads the keys
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: true,
crossSigningVerifiedBefore: false,
});
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
});
+1 -1
View File
@@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise<IRecoveryKey> {
decryption.free();
return {
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey, key: undefined },
keyInfo: { pubkey: storagePublicKey, key: undefined! },
privateKey: storagePrivateKey,
};
}
@@ -93,7 +93,7 @@ describe.each([
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(RoomKeyRequestState.Sent);
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r);
});
});
+32 -32
View File
@@ -21,9 +21,9 @@ import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient';
import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger';
import { ICreateClientOpts } from '../../../src/client';
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
@@ -41,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
await client.initCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({});
return client;
}
@@ -93,11 +93,11 @@ describe("Secrets", function() {
},
},
);
alice.crypto.crossSigningInfo.setKeys({
alice.crypto!.crossSigningInfo.setKeys({
master: signingkeyInfo,
});
const secretStorage = alice.crypto.secretStorage;
const secretStorage = alice.crypto!.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents) {
@@ -113,7 +113,7 @@ describe("Secrets", function() {
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([
new MatrixEvent({
@@ -200,7 +200,7 @@ describe("Secrets", function() {
await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo');
expect(accountData.getContent().encrypted).toBeTruthy();
expect(accountData!.getContent().encrypted).toBeTruthy();
alice.stopClient();
});
@@ -233,29 +233,29 @@ describe("Secrets", function() {
},
);
const vaxDevice = vax.client.crypto.olmDevice;
const osborne2Device = osborne2.client.crypto.olmDevice;
const secretStorage = osborne2.client.crypto.secretStorage;
const vaxDevice = vax.client.crypto!.olmDevice;
const osborne2Device = osborne2.client.crypto!.olmDevice;
const secretStorage = osborne2.client.crypto!.secretStorage;
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
"ed25519:VAX": vaxDevice.deviceEd25519Key!,
"curve25519:VAX": vaxDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.VERIFIED,
},
});
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
"ed25519:Osborne2": osborne2Device.deviceEd25519Key!,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key!,
},
},
});
@@ -264,13 +264,13 @@ describe("Secrets", function() {
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished();
await vax.client.crypto.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key,
await vax.client.crypto!.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key!,
Object.values(otks)[0],
);
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used
@@ -328,7 +328,7 @@ describe("Secrets", function() {
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
@@ -339,8 +339,8 @@ describe("Secrets", function() {
createSecretStorageKey,
});
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto!.secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@@ -437,6 +437,7 @@ describe("Secrets", function() {
return [keyId, secretStorageKeys[keyId]];
}
}
return null;
},
},
},
@@ -486,7 +487,7 @@ describe("Secrets", function() {
},
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
@@ -528,16 +529,15 @@ describe("Secrets", function() {
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
.toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
.getContent() as ISecretStorageKeyInfo;
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
@@ -572,6 +572,7 @@ describe("Secrets", function() {
return [keyId, secretStorageKeys[keyId]];
}
}
return null;
},
},
},
@@ -630,7 +631,7 @@ describe("Secrets", function() {
},
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
@@ -672,14 +673,13 @@ describe("Secrets", function() {
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
await alice.bootstrapSecretStorage({});
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
@@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS],
},
);
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
algorithms: [],
+50 -47
View File
@@ -17,16 +17,17 @@ limitations under the License.
import "../../../olm-loader";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
@@ -75,13 +76,13 @@ describe("SAS verification", function() {
});
describe("verification", () => {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
let clearTestClientTimeouts;
let alice: TestClient;
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: Verification<any, any>;
let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void;
beforeEach(async () => {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@@ -94,8 +95,8 @@ describe("SAS verification", function() {
},
);
const aliceDevice = alice.client.crypto.olmDevice;
const bobDevice = bob.client.crypto.olmDevice;
const aliceDevice = alice.client.crypto!.olmDevice;
const bobDevice = bob.client.crypto!.olmDevice;
ALICE_DEVICES = {
Osborne2: {
@@ -121,26 +122,26 @@ describe("SAS verification", function() {
},
};
alice.client.crypto.deviceList.storeDevicesForUser(
alice.client.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
alice.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
bob.client.crypto.deviceList.storeDevicesForUser(
bob.client.crypto!.deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES,
);
bob.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -156,14 +157,14 @@ describe("SAS verification", function() {
}
}
});
resolve(request.verifier);
resolve(request.verifier!);
});
});
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId,
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
);
aliceVerifier.on("show_sas", (e) => {
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
@@ -195,9 +196,9 @@ describe("SAS verification", function() {
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
.key_agreement_protocol;
}
return origSendToDevice(type, map);
@@ -219,8 +220,8 @@ describe("SAS verification", function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
// make sure that it uses the preferred method
@@ -230,10 +231,10 @@ describe("SAS verification", function() {
// make sure Alice and Bob verified each other
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should be able to verify using the old base64", async () => {
@@ -248,7 +249,7 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()][bob.client.deviceId]
map[bob.client.getUserId()!][bob.client.deviceId!]
.message_authentication_codes = ['hkdf-hmac-sha256'];
}
return aliceOrigSendToDevice(type, map);
@@ -256,7 +257,7 @@ describe("SAS verification", function() {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
@@ -278,18 +279,18 @@ describe("SAS verification", function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
expect(aliceDevice!.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async () => {
@@ -304,7 +305,7 @@ describe("SAS verification", function() {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()][bob.client.deviceId]
map[bob.client.getUserId()!][bob.client.deviceId!]
.message_authentication_codes = ['hmac-sha256'];
}
return aliceOrigSendToDevice(type, map);
@@ -312,7 +313,7 @@ describe("SAS verification", function() {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
@@ -334,18 +335,18 @@ describe("SAS verification", function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should verify a cross-signing key", async () => {
@@ -361,9 +362,11 @@ describe("SAS verification", function() {
await resetCrossSigningKeys(bob.client);
bob.client.crypto.deviceList.storeCrossSigningForUser(
bob.client.crypto!.deviceList.storeCrossSigningForUser(
"@alice:example.com", {
keys: alice.client.crypto.crossSigningInfo.keys,
keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
},
);
@@ -415,10 +418,10 @@ describe("SAS verification", function() {
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier.on("show_sas", (e) => {
request.verifier!.on("show_sas", (e) => {
e.mismatch();
});
resolve(request.verifier);
resolve(request.verifier!);
});
});
@@ -464,7 +467,7 @@ describe("SAS verification", function() {
},
);
alice.client.crypto.setDeviceVerification = jest.fn();
alice.client.crypto!.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
@@ -482,7 +485,7 @@ describe("SAS verification", function() {
return Promise.resolve();
};
bob.client.crypto.setDeviceVerification = jest.fn();
bob.client.crypto!.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
@@ -565,7 +568,7 @@ describe("SAS verification", function() {
]);
// make sure Alice and Bob verified each other
expect(alice.client.crypto.setDeviceVerification)
expect(alice.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith(
bob.client.getUserId(),
bob.client.deviceId,
@@ -574,7 +577,7 @@ describe("SAS verification", function() {
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto.setDeviceVerification)
expect(bob.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
+1 -1
View File
@@ -41,7 +41,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client.crypto) :
event.attemptDecryption(client.crypto!) :
Promise.resolve();
decryptionPromise.then(
@@ -131,7 +131,11 @@ function makeRemoteEcho(event) {
}));
}
async function distributeEvent(ownRequest, theirRequest, event) {
async function distributeEvent(
ownRequest: VerificationRequest,
theirRequest: VerificationRequest,
event: MatrixEvent,
): Promise<void> {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event),
ownRequest,
+1 -1
View File
@@ -32,7 +32,7 @@ describe("eventMapperFor", function() {
fetchFn: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
return rooms.find(r => r.roomId === roomId) ?? null;
},
} as IStore,
scheduler: {
+9 -9
View File
@@ -45,13 +45,13 @@ describe('EventTimelineSet', () => {
it('should return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
messageEvent.getId()!,
"m.in_reply_to",
EventType.RoomMessage,
);
expect(relations).toBeDefined();
expect(relations.getRelations().length).toBe(1);
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
expect(relations!.getRelations().length).toBe(1);
expect(relations!.getRelations()[0].getId()).toBe(replyEvent.getId());
});
};
@@ -193,7 +193,7 @@ describe('EventTimelineSet', () => {
it('should not return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
messageEvent.getId()!,
"m.in_reply_to",
EventType.RoomMessage,
);
@@ -236,7 +236,7 @@ describe('EventTimelineSet', () => {
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
"event_id": root.getId()!,
},
"rel_type": "m.thread",
},
@@ -278,14 +278,14 @@ describe('EventTimelineSet', () => {
});
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
const event = mkThreadResponse(messageEvent);
@@ -310,7 +310,7 @@ describe('EventTimelineSet', () => {
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId()!);
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
@@ -318,7 +318,7 @@ describe('EventTimelineSet', () => {
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId()!);
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
+6 -6
View File
@@ -21,7 +21,7 @@ describe("EventTimeline", function() {
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
return new EventTimeline(timelineSet);
};
@@ -341,11 +341,11 @@ describe("EventTimeline", function() {
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId());
let ev = timeline.removeEvent(events[0].getId()!);
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
ev = timeline.removeEvent(events[1].getId());
ev = timeline.removeEvent(events[1].getId()!);
expect(ev).toBe(events[1]);
expect(timeline.getEvents().length).toEqual(0);
});
@@ -357,11 +357,11 @@ describe("EventTimeline", function() {
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[2].getId());
timeline.removeEvent(events[2].getId()!);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[1].getId());
timeline.removeEvent(events[1].getId()!);
expect(timeline.getEvents().length).toEqual(1);
expect(timeline.getBaseIndex()).toEqual(0);
});
@@ -372,7 +372,7 @@ describe("EventTimeline", function() {
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.removeEvent(events[0].getId());
timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
-65
View File
@@ -1,65 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../src/models/event";
describe("MatrixEvent", () => {
describe(".attemptDecryption", () => {
let encryptedEvent;
const eventId = 'test_encrypted_event';
beforeEach(() => {
encryptedEvent = new MatrixEvent({
event_id: eventId,
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
const crypto = {
decryptEvent: jest.fn()
.mockImplementationOnce(() => {
// schedule a second decryption attempt while
// the first one is still running.
encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
})
.mockImplementationOnce(() => {
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}),
};
await encryptedEvent.attemptDecryption(crypto);
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
});
});
});
+16
View File
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils";
+10
View File
@@ -220,4 +220,14 @@ describe("FetchHttpApi", () => {
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
describe("authedRequest", () => {
it("should not include token if unset", () => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
});
});
+3 -3
View File
@@ -28,7 +28,7 @@ describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Partial<Writeable<XMLHttpRequest>>;
let xhr: Writeable<XMLHttpRequest>;
let upload: Promise<UploadResponse>;
const DONE = 0;
@@ -44,7 +44,7 @@ describe("MatrixHttpApi", () => {
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
};
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
@@ -62,7 +62,7 @@ describe("MatrixHttpApi", () => {
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined;
global.XMLHttpRequest = undefined!;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
+200 -202
View File
@@ -36,7 +36,7 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { ContentHelpers, EventTimeline, Room } from "../../src";
import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
@@ -88,21 +88,22 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
};
let httpLookups = [
// items are objects which look like:
// {
// method: "GET",
// path: "/initialSync",
// data: {},
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
// expectBody: {} // additional expects on the body
// expectQueryParams: {} // additional expects on query params
// thenCall: function(){} // function to call *AFTER* returning response.
// }
// items are popped off when processed and block if no items left.
];
// items are popped off when processed and block if no items left.
let httpLookups: {
method: string;
path: string;
data?: object;
error?: object;
expectBody?: object;
expectQueryParams?: object;
thenCall?: Function;
}[] = [];
let acceptKeepalives: boolean;
let pendingLookup = null;
let pendingLookup: {
promise: Promise<any>;
method: string;
path: string;
} | null = null;
function httpReq(method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({
@@ -144,7 +145,7 @@ describe("MatrixClient", function() {
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
expect(qp[k]).toEqual(next.expectQueryParams![k]);
});
}
@@ -155,9 +156,9 @@ describe("MatrixClient", function() {
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
errcode: (<MatrixError>next.error).errcode,
httpStatus: (<MatrixError>next.error).httpStatus,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
});
@@ -230,6 +231,130 @@ describe("MatrixClient", function() {
client.stopClient();
});
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
const threadId = "$threadId:server";
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const content = {
body,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -254,7 +379,7 @@ describe("MatrixClient", function() {
type: UNSTABLE_MSC3088_PURPOSE.unstable,
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
},
},
{
@@ -299,7 +424,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
},
});
} else {
@@ -359,7 +484,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
[UNSTABLE_MSC3088_ENABLED.unstable!]: true,
},
});
} else {
@@ -393,7 +518,7 @@ describe("MatrixClient", function() {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
[UNSTABLE_MSC3088_ENABLED.unstable!]: false,
},
});
} else {
@@ -599,14 +724,14 @@ describe("MatrixClient", function() {
}
it("should transition null -> PREPARED after the first /sync", function(done) {
const expectedStates = [];
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition null -> ERROR after a failed /filter", function(done) {
const expectedStates = [];
const expectedStates: [string, string | null][] = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
@@ -620,36 +745,35 @@ describe("MatrixClient", function() {
// Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like
xit("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) {
const expectedStates: [string, string | null][] = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
const expectedStates = [];
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
@@ -658,7 +782,7 @@ describe("MatrixClient", function() {
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
acceptKeepalives = false;
const expectedStates = [];
const expectedStates: [string, string | null][] = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
@@ -675,37 +799,35 @@ describe("MatrixClient", function() {
client.startClient();
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) {
const expectedStates: [string, string | null][] = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) {
const expectedStates: [string, string | null][] = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
const expectedStates = [];
const expectedStates: [string, string | null][] = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
@@ -779,130 +901,6 @@ describe("MatrixClient", function() {
});
});
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
const threadId = "$threadId:server";
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const content = {
body,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
});
describe("redactEvent", () => {
const roomId = "!room:example.org";
const mockRoom = {
+1 -1
View File
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: (): string => null,
replacingEventId: (): string | undefined => undefined,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
+12 -12
View File
@@ -135,7 +135,7 @@ describe("MSC3089TreeSpace", () => {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.errcode).toEqual("M_FORBIDDEN");
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
}
expect(fn).toHaveBeenCalledTimes(1);
@@ -513,7 +513,7 @@ describe("MSC3089TreeSpace", () => {
function expectOrder(childRoomId: string, order: number) {
const child = childTrees.find(c => c.roomId === childRoomId);
expect(child).toBeDefined();
expect(child.getOrder()).toEqual(order);
expect(child!.getOrder()).toEqual(order);
}
function makeMockChildRoom(roomId: string): Room {
@@ -608,7 +608,7 @@ describe("MSC3089TreeSpace", () => {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toEqual("Cannot set order of top level spaces currently");
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
}
});
@@ -706,7 +706,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(1);
await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -743,7 +743,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(1);
await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -771,7 +771,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(2);
await treeA!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -800,7 +800,7 @@ describe("MSC3089TreeSpace", () => {
const treeB = childTrees.find(c => c.roomId === b);
expect(treeB).toBeDefined();
await treeB.setOrder(2);
await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -829,7 +829,7 @@ describe("MSC3089TreeSpace", () => {
const treeC = childTrees.find(ch => ch.roomId === c);
expect(treeC).toBeDefined();
await treeC.setOrder(1);
await treeC!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -858,7 +858,7 @@ describe("MSC3089TreeSpace", () => {
const treeB = childTrees.find(ch => ch.roomId === b);
expect(treeB).toBeDefined();
await treeB.setOrder(2);
await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -903,7 +903,7 @@ describe("MSC3089TreeSpace", () => {
url: mxc,
file: fileInfo,
metadata: true, // additional content from test
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
});
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
@@ -965,7 +965,7 @@ describe("MSC3089TreeSpace", () => {
expect(contents).toMatchObject({
...content,
"m.new_content": content,
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
});
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
@@ -1010,7 +1010,7 @@ describe("MSC3089TreeSpace", () => {
const file = tree.getFile(fileEventId);
expect(file).toBeDefined();
expect(file.indexEvent).toBe(fileEvent);
expect(file!.indexEvent).toBe(fileEvent);
});
it('should return falsy for unknown files', () => {
+1 -1
View File
@@ -263,7 +263,7 @@ describe('Beacon', () => {
roomId,
);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
beacon.update(oldUpdateEvent);
// didnt update
+46 -1
View File
@@ -115,8 +115,53 @@ describe('MatrixEvent', () => {
});
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId(), reason: null });
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId()!, reason: null });
await prom;
});
});
describe(".attemptDecryption", () => {
let encryptedEvent;
const eventId = 'test_encrypted_event';
beforeEach(() => {
encryptedEvent = new MatrixEvent({
event_id: eventId,
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
const crypto = {
decryptEvent: jest.fn()
.mockImplementationOnce(() => {
// schedule a second decryption attempt while
// the first one is still running.
encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
})
.mockImplementationOnce(() => {
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}),
};
await encryptedEvent.attemptDecryption(crypto);
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
});
});
});
+43 -23
View File
@@ -163,6 +163,22 @@ describe('NotificationService', function() {
"enabled": true,
"rule_id": ".m.rule.room_one_to_one",
},
{
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
},
],
"room": [],
"sender": [],
@@ -209,32 +225,32 @@ describe('NotificationService', function() {
msgtype: "m.text",
},
});
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules!);
pushProcessor = new PushProcessor(matrixClient);
});
// User IDs
it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
testEvent.event.content!.body = "Hello @ali:matrix.org, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?";
testEvent.event.content!.body = "Hello @ali, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?";
testEvent.event.content!.body = "Hello ali, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
testEvent.event.content!.body = "Hello @AlI:matrix.org, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -242,13 +258,13 @@ describe('NotificationService', function() {
// Display names
it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?";
testEvent.event.content!.body = "Hello Alice M, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?";
testEvent.event.content!.body = "Hello ALICE M, how are you?";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -256,43 +272,43 @@ describe('NotificationService', function() {
// Bing words
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
testEvent.event.content!.body = "I really like coffee";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great";
testEvent.event.content!.body = "Coffee is great";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think.";
testEvent.event.content!.body = "It was foomahbar I think.";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!";
testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
testEvent.event.content!.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke";
testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
testEvent.event.content!.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
@@ -316,7 +332,7 @@ describe('NotificationService', function() {
// invalid
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
testEvent.event.content!.body = { "foo": "bar" };
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
@@ -337,7 +353,11 @@ describe('NotificationService', function() {
}, testEvent)).toBe(true);
});
describe("performCustomEventHandling()", () => {
describe("group call started push rule", () => {
beforeEach(() => {
matrixClient.pushRules!.global!.underride!.find(r => r.rule_id === ".m.rule.fallback")!.enabled = false;
});
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
testEvent = utils.mkEvent({
type: "org.matrix.msc3401.call",
@@ -353,15 +373,15 @@ describe('NotificationService', function() {
};
const assertDoesNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeTruthy();
expect(actions.tweaks.sound).toBeTruthy();
expect(actions.tweaks.highlight).toBeFalsy();
expect(actions?.notify).toBeTruthy();
expect(actions?.tweaks?.sound).toBeTruthy();
expect(actions?.tweaks?.highlight).toBeFalsy();
};
const assertDoesNotNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeFalsy();
expect(actions.tweaks.sound).toBeFalsy();
expect(actions.tweaks.highlight).toBeFalsy();
expect(actions?.notify).toBeFalsy();
expect(actions?.tweaks?.sound).toBeFalsy();
expect(actions?.tweaks?.highlight).toBeFalsy();
};
it.each(
+13 -13
View File
@@ -140,11 +140,11 @@ describe.each([
],
});
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
expect(httpBackend.flushSync(null, 1)).toEqual(1);
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
expect(httpBackend.flushSync(null, 1)).toEqual(1);
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
// flush, as per comment in first test
await flushPromises();
@@ -164,7 +164,7 @@ describe.each([
],
});
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
expect(httpBackend.flushSync(null, 1)).toEqual(1);
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
// Asserting that another request is never made is obviously
// a bit tricky - we just flush the queue what should hopefully
@@ -200,7 +200,7 @@ describe.each([
],
});
await flushAndRunTimersUntil(() => httpBackend.requests.length > 0);
expect(httpBackend.flushSync(null, 1)).toEqual(1);
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
await flushPromises();
logger.info("Advancing clock to just before expected retry time...");
@@ -215,7 +215,7 @@ describe.each([
jest.advanceTimersByTime(2000);
await flushPromises();
expect(httpBackend.flushSync(null, 1)).toEqual(1);
expect(httpBackend.flushSync(undefined, 1)).toEqual(1);
});
it("retries on retryImmediately()", async function() {
@@ -223,7 +223,7 @@ describe.each([
versions: ["r0.0.1"],
});
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
httpBackend.when(
"PUT", "/sendToDevice/org.example.foo/",
@@ -239,13 +239,13 @@ describe.each([
FAKE_MSG,
],
});
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
await flushPromises();
client.retryImmediately();
// longer timeout here to try & avoid flakiness
expect(await httpBackend.flush(null, 1, 3000)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 3000)).toEqual(1);
});
it("retries on when client is started", async function() {
@@ -269,13 +269,13 @@ describe.each([
FAKE_MSG,
],
});
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
await flushPromises();
client.stopClient();
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
});
it("retries when a message is retried", async function() {
@@ -283,7 +283,7 @@ describe.each([
versions: ["r0.0.1"],
});
await Promise.all([client.startClient(), httpBackend.flush(null, 1, 20)]);
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
httpBackend.when(
"PUT", "/sendToDevice/org.example.foo/",
@@ -300,7 +300,7 @@ describe.each([
],
});
expect(await httpBackend.flush(null, 1, 1)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
await flushPromises();
const dummyEvent = new MatrixEvent({
@@ -311,7 +311,7 @@ describe.each([
} as unknown as Room;
client.resendEvent(dummyEvent, mockRoom);
expect(await httpBackend.flush(null, 1, 20)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
});
it("splits many messages into multiple HTTP requests", async function() {
+4 -4
View File
@@ -97,7 +97,7 @@ describe("Read receipt", () => {
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
$eventId: threadEvent.getId()!,
}),
).check((request) => {
expect(request.data.thread_id).toEqual(THREAD_ID);
@@ -115,7 +115,7 @@ describe("Read receipt", () => {
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: roomEvent.getId(),
$eventId: roomEvent.getId()!,
}),
).check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
@@ -133,7 +133,7 @@ describe("Read receipt", () => {
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
$eventId: threadEvent.getId()!,
}),
).check((request) => {
expect(request.data.thread_id).toBeUndefined();
@@ -151,7 +151,7 @@ describe("Read receipt", () => {
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
$eventId: threadEvent.getId()!,
}),
).check((request) => {
expect(request.data).toEqual({});
+17 -1
View File
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as callbacks from "../../src/realtime-callbacks";
let wallTime = 1234567890;
@@ -37,7 +53,7 @@ describe("realtime-callbacks", function() {
it("should set 'this' to the global object", function() {
let passed = false;
const callback = function() {
const callback = function(this: typeof global) {
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
passed = true;
+33 -8
View File
@@ -18,10 +18,11 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations";
import { TestClient } from "../TestClient";
describe("Relations", function() {
it("should deduplicate annotations", function() {
const room = new Room("room123", null, null);
const room = new Room("room123", null!, null!);
const relations = new Relations("m.annotation", "m.reaction", room);
// Create an instance of an annotation
@@ -43,7 +44,7 @@ describe("Relations", function() {
// Add the event once and check results
{
relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey();
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
@@ -53,7 +54,7 @@ describe("Relations", function() {
// Add the event again and expect the same
{
relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey();
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
@@ -66,7 +67,7 @@ describe("Relations", function() {
// Add the event again and expect the same
{
relations.addEvent(eventB);
const annotationsByKey = relations.getSortedAnnotationsByKey();
const annotationsByKey = relations.getSortedAnnotationsByKey()!;
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
@@ -98,7 +99,7 @@ describe("Relations", function() {
// Add the target event first, then the relation event
{
const room = new Room("room123", null, null);
const room = new Room("room123", null!, null!);
const relationsCreated = new Promise(resolve => {
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
@@ -112,7 +113,7 @@ describe("Relations", function() {
// Add the relation event first, then the target event
{
const room = new Room("room123", null, null);
const room = new Room("room123", null!, null!);
const relationsCreated = new Promise(resolve => {
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
@@ -126,7 +127,7 @@ describe("Relations", function() {
});
it("should re-use Relations between all timeline sets in a room", async () => {
const room = new Room("room123", null, null);
const room = new Room("room123", null!, null!);
const timelineSet1 = new EventTimelineSet(room);
const timelineSet2 = new EventTimelineSet(room);
expect(room.relations).toBe(timelineSet1.relations);
@@ -135,7 +136,7 @@ describe("Relations", function() {
it("should ignore m.replace for state events", async () => {
const userId = "@bob:example.com";
const room = new Room("room123", null, userId);
const room = new Room("room123", null!, userId);
const relations = new Relations("m.replace", "m.room.topic", room);
// Create an instance of a state event with rel_type m.replace
@@ -179,4 +180,28 @@ describe("Relations", function() {
expect(badlyEditedTopic.replacingEvent()).toBe(null);
expect(badlyEditedTopic.getContent().topic).toBe("topic");
});
it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => {
const userId = "@user:server";
const room = new Room("room123", new TestClient(userId).client, userId);
const relations = new Relations("m.replace", "m.room.message", room);
// Create an instance of an annotation
const eventData = {
"sender": "@bob:example.com",
"type": "m.room.message",
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
"content": {
"m.relates_to": {
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
"rel_type": "m.replace",
},
},
};
const eventA = new MatrixEvent(eventData);
relations.addEvent(eventA);
expect(relations.getSortedAnnotationsByKey()).toBeNull();
});
});
+92
View File
@@ -0,0 +1,92 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../../../src/logger";
import {
RendezvousFailureListener,
RendezvousFailureReason,
RendezvousTransport,
RendezvousTransportDetails,
} from "../../../src/rendezvous";
import { sleep } from '../../../src/utils';
export class DummyTransport<D extends RendezvousTransportDetails, T> implements RendezvousTransport<T> {
otherParty?: DummyTransport<D, T>;
etag?: string;
lastEtagReceived?: string;
data: T | undefined;
ready = false;
cancelled = false;
constructor(private name: string, private mockDetails: D) {}
onCancelled?: RendezvousFailureListener;
details(): Promise<RendezvousTransportDetails> {
return Promise.resolve(this.mockDetails);
}
async send(data: T): Promise<void> {
logger.info(
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
JSON.stringify(data)} where etag matches ${this.etag}`,
);
// eslint-disable-next-line no-constant-condition
while (!this.cancelled) {
if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) {
this.data = data;
this.etag = Math.random().toString();
this.lastEtagReceived = this.etag;
this.otherParty!.etag = this.etag;
this.otherParty!.data = data;
logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`);
return;
}
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
await sleep(250);
}
}
async receive(): Promise<T | undefined> {
logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`);
// eslint-disable-next-line no-constant-condition
while (!this.cancelled) {
if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) {
this.lastEtagReceived = this.etag;
logger.info(
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
`${JSON.stringify(this.data)} with etag ${this.etag}`,
);
return this.data;
}
logger.info(`[${this.name}] Sleeping to retry receive after etag ${
this.lastEtagReceived} as remote is ${this.etag}`);
await sleep(250);
}
return undefined;
}
cancel(reason: RendezvousFailureReason): Promise<void> {
this.cancelled = true;
this.onCancelled?.(reason);
return Promise.resolve();
}
cleanup() {
this.cancelled = true;
}
}
+172
View File
@@ -0,0 +1,172 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels';
import { decodeBase64 } from '../../../src/crypto/olmlib';
import { DummyTransport } from './DummyTransport';
function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'dummy' });
}
describe('ECDHv1', function() {
beforeAll(async function() {
await global.Olm.init();
});
describe('with crypto', () => {
it("initiator wants to sign in", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await alice.send(message);
const bobReceive = await bob.receive();
expect(bobReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("initiator wants to reciprocate", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await bob.send(message);
const aliceReceive = await alice.receive();
expect(aliceReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("double connect", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
expect(alice.connect()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("closed", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
alice.close();
expect(alice.connect()).rejects.toThrow();
expect(alice.send({})).rejects.toThrow();
expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("require ciphertext", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(bob.receive()).rejects.toThrowError();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("ciphertext before set up", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob');
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(alice.receive()).rejects.toThrowError();
await alice.cancel(RendezvousFailureReason.Unknown);
});
});
});
+602
View File
@@ -0,0 +1,602 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MockHttpBackend from "matrix-mock-request";
import '../../olm-loader';
import {
MSC3906Rendezvous,
RendezvousCode,
RendezvousFailureReason,
RendezvousIntent,
} from "../../../src/rendezvous";
import {
ECDHv1RendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv1RendezvousChannel,
} from "../../../src/rendezvous/channels";
import { MatrixClient } from "../../../src";
import {
MSC3886SimpleHttpRendezvousTransport,
MSC3886SimpleHttpRendezvousTransportDetails,
} from "../../../src/rendezvous/transports";
import { DummyTransport } from "./DummyTransport";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { logger } from "../../../src/logger";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
function makeMockClient(opts: {
userId: string;
deviceId: string;
deviceKey?: string;
msc3882Enabled: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
verificationFunction?: (
userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean,
) => void;
crossSigningIds?: Record<string, string>;
}): MatrixClient {
return {
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.msc3882Enabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
},
getUserId() { return opts.userId; },
getDeviceId() { return opts.deviceId; },
getDeviceEd25519Key() { return opts.deviceKey; },
baseUrl: "https://example.com",
crypto: {
getStoredDevice(userId: string, deviceId: string) {
return opts.devices?.[deviceId] ?? null;
},
setDeviceVerification: opts.verificationFunction,
crossSigningInfo: {
getId(key: string) {
return opts.crossSigningIds?.[key];
},
},
},
} as unknown as MatrixClient;
}
function makeTransport(name: string, uri = 'https://test.rz/123456') {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'http.v1', uri });
}
describe("Rendezvous", function() {
beforeAll(async function() {
await global.Olm.init();
});
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetchFn;
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
beforeEach(function() {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
transports = [];
});
afterEach(function() {
transports.forEach(x => x.cleanup());
});
it("generate and cancel", async function() {
const alice = makeMockClient({
userId: "@alice:example.com",
deviceId: "DEVICEID",
msc3886Enabled: false,
msc3882Enabled: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
client: alice,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
const codePromise = aliceRz.generateCode();
await httpBackend.flush('');
await aliceRz.generateCode();
expect(typeof aliceRz.code).toBe('string');
await codePromise;
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri)
.toEqual("https://fallbackserver/rz/123");
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
await httpBackend.flush('');
expect(cancelPromise).resolves.toBeUndefined();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequests();
await aliceRz.close();
});
it("no protocols", async function() {
const aliceTransport = makeTransport('Alice');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.finish',
outcome: 'unsupported',
});
})();
await aliceStartProm;
await bobStartPromise;
});
it("new device declines protocol", async function() {
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.progress',
protocols: ['org.matrix.msc3906.login_token'],
});
await bobEcdh.send({ type: 'm.login.finish', outcome: 'unsupported' });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("new device declines protocol", async function() {
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.progress',
protocols: ['org.matrix.msc3906.login_token'],
});
await bobEcdh.send({ type: 'm.login.progress', protocol: 'bad protocol' });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("decline on existing device", async function() {
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.progress',
protocols: ['org.matrix.msc3906.login_token'],
});
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
})();
await aliceStartProm;
await bobStartPromise;
await aliceRz.declineLoginOnExistingDevice();
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: 'm.login.finish', outcome: 'declined' });
});
it("approve on existing device + no verification", async function() {
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.progress',
protocols: ['org.matrix.msc3906.login_token'],
});
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobCompleteProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success' });
})();
await confirmProm;
await bobCompleteProm;
});
async function completeLogin(devices: Record<string, Partial<DeviceInfo>>) {
const aliceTransport = makeTransport('Alice', 'https://test.rz/123456');
const bobTransport = makeTransport('Bob', 'https://test.rz/999999');
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const aliceVerification = jest.fn();
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3886Enabled: false,
devices,
deviceKey: 'aaaa',
verificationFunction: aliceVerification,
crossSigningIds: {
master: 'mmmmm',
},
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info('Bob waiting for protocols');
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: 'm.login.progress',
protocols: ['org.matrix.msc3906.login_token'],
});
await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobLoginProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl });
await bobEcdh.send({ type: 'm.login.finish', outcome: 'success', device_id: 'BOB', device_key: 'bbbb' });
})();
expect(await confirmProm).toEqual('BOB');
await bobLoginProm;
return {
aliceTransport,
aliceEcdh,
aliceRz,
bobTransport,
bobEcdh,
};
}
it("approve on existing device + verification", async function() {
const { bobEcdh, aliceRz } = await completeLogin({
BOB: {
getFingerprint: () => "bbbb",
},
});
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
const bobVerifyProm = (async () => {
const verified = await bobEcdh.receive();
expect(verified).toEqual({
type: 'm.login.finish',
outcome: 'verified',
verifying_device_id: 'ALICE',
verifying_device_key: 'aaaa',
master_key: 'mmmmm',
});
})();
await verifyProm;
await bobVerifyProm;
});
it("device not online within timeout", async function() {
const { aliceRz } = await completeLogin({});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
});
it("device appears online within timeout", async function() {
const devices: Record<string, Partial<DeviceInfo>> = {};
const { aliceRz } = await completeLogin(devices);
// device appears after 1 second
setTimeout(() => {
devices.BOB = {
getFingerprint: () => "bbbb",
};
}, 1000);
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
});
it("device appears online after timeout", async function() {
const devices: Record<string, Partial<DeviceInfo>> = {};
const { aliceRz } = await completeLogin(devices);
// device appears after 1 second
setTimeout(() => {
devices.BOB = {
getFingerprint: () => "bbbb",
};
}, 1500);
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
});
it("mismatched device key", async function() {
const { aliceRz } = await completeLogin({
BOB: {
getFingerprint: () => "XXXX",
},
});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
});
});
@@ -0,0 +1,451 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MockHttpBackend from "matrix-mock-request";
import type { MatrixClient } from "../../../src";
import { RendezvousFailureReason } from "../../../src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
function makeMockClient(opts: { userId: string, deviceId: string, msc3886Enabled: boolean}): MatrixClient {
return {
doesServerSupportUnstableFeature(feature: string) {
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
},
getUserId() { return opts.userId; },
getDeviceId() { return opts.deviceId; },
requestLoginToken() {
return Promise.resolve({ login_token: "token" });
},
baseUrl: "https://example.com",
} as unknown as MatrixClient;
}
describe("SimpleHttpRendezvousTransport", function() {
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
beforeEach(function() {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
});
async function postAndCheckLocation(
msc3886Enabled: boolean,
fallbackRzServer: string,
locationResponse: string,
expectedFinalLocation: string,
) {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
{ // initial POST
const expectedPostLocation = msc3886Enabled ?
`${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous` :
fallbackRzServer;
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", expectedPostLocation).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: locationResponse,
},
},
};
await httpBackend.flush('');
await prom;
}
const details = await simpleHttpTransport.details();
expect(details.uri).toBe(expectedFinalLocation);
{ // first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", expectedFinalLocation).response = {
body: {},
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
},
},
};
await httpBackend.flush('');
expect(await prom).toEqual({});
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
}
}
it("should throw an error when no server available", function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
});
it("POST to fallback server", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush('');
expect(await prom).toStrictEqual(undefined);
});
it("POST with no location", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
expect(prom).rejects.toThrowError();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {},
},
};
await httpBackend.flush('');
});
it("POST with absolute path response", async function() {
await postAndCheckLocation(
false,
"https://fallbackserver/rz",
"/123",
"https://fallbackserver/123",
);
});
it("POST to built-in MSC3886 implementation", async function() {
await postAndCheckLocation(
true,
"https://fallbackserver/rz",
"123",
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
);
});
it("POST with relative path response including parent", async function() {
await postAndCheckLocation(
false,
"https://fallbackserver/rz/abc",
"../xyz/123",
"https://fallbackserver/rz/xyz/123",
);
});
it("POST with relative path response including parent", async function() {
await postAndCheckLocation(
false,
"https://fallbackserver/rz/abc",
"../xyz/123",
"https://fallbackserver/rz/xyz/123",
);
});
it("POST to follow 307 to other server", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 307,
headers: {
location: "https://redirected.fallbackserver/rz",
},
},
};
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://redirected.fallbackserver/rz/123",
etag: "aaa",
},
},
};
await httpBackend.flush('');
expect(await prom).toStrictEqual(undefined);
});
it("POST and GET", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{ // initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush('');
expect(await prom).toStrictEqual(undefined);
}
{ // first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "aaa",
},
},
};
await httpBackend.flush('');
expect(await prom).toEqual({ foo: "baa" });
}
{ // subsequent GET which should have etag from previous request
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-none-match"]).toEqual("aaa");
}).response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "bbb",
},
},
};
await httpBackend.flush('');
expect(await prom).toEqual({ foo: "baa" });
}
});
it("POST and PUTs", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{ // initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush('', 1);
await prom;
}
{ // first PUT without etag
const prom = simpleHttpTransport.send({ a: "b" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
expect(headers["if-match"]).toBeUndefined();
expect(data).toEqual({ a: "b" });
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
"etag": "aaa",
},
},
};
await httpBackend.flush('', 1);
await prom;
}
{ // subsequent PUT which should have etag from previous request
const prom = simpleHttpTransport.send({ c: "d" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-match"]).toEqual("aaa");
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
"etag": "bbb",
},
},
};
await httpBackend.flush('', 1);
await prom;
}
});
it("POST and DELETE", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{ // Create
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush('');
expect(await prom).toStrictEqual(undefined);
}
{ // Cancel
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
await httpBackend.flush('');
await prom;
}
});
it("details before ready", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.details()).rejects.toThrowError();
});
it("send after cancelled", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
});
it("receive before ready", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.receive()).rejects.toThrowError();
});
it("404 failure callback", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 404,
headers: {},
},
};
await httpBackend.flush('', 1);
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
});
it("404 failure callback mapped to expired", async function() {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
{ // initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
},
},
};
await httpBackend.flush('');
await prom;
}
{ // GET with 404 to simulate expiry
expect(simpleHttpTransport.receive()).resolves.toBeUndefined();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 404,
headers: {},
},
};
await httpBackend.flush('');
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
}
});
});
+1 -1
View File
@@ -172,7 +172,7 @@ describe("RoomState", function() {
state.on(RoomStateEvent.Members, function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
expect(mem).toEqual(state.getMember(ev.getSender()));
expect(mem).toEqual(state.getMember(ev.getSender()!));
emitCount += 1;
});
state.setStateEvents(memberEvents);
+292 -92
View File
@@ -24,7 +24,7 @@ import {
DuplicateStrategy,
EventStatus,
EventTimelineSet,
EventType,
EventType, IStateEventWithRoomId,
JoinRule,
MatrixEvent,
MatrixEventEvent,
@@ -39,7 +39,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts";
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { WrappedReceipt } from "../../src/models/read-receipt";
import { Crypto } from "../../src/crypto";
@@ -66,7 +66,7 @@ describe("Room", function() {
"body": "Reply :: " + Math.random(),
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
"event_id": target.getId()!,
},
},
},
@@ -84,7 +84,7 @@ describe("Room", function() {
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: target.getId(),
event_id: target.getId()!,
},
},
}, room.client);
@@ -97,9 +97,9 @@ describe("Room", function() {
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"event_id": root.getId()!,
"m.in_reply_to": {
"event_id": root.getId(),
"event_id": root.getId()!,
},
"rel_type": "m.thread",
},
@@ -114,7 +114,7 @@ describe("Room", function() {
content: {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": target.getId(),
"event_id": target.getId()!,
"key": Math.random().toString(),
},
},
@@ -125,7 +125,7 @@ describe("Room", function() {
type: EventType.RoomRedaction,
user: userA,
room: roomId,
redacts: target.getId(),
redacts: target.getId()!,
content: {},
}, room.client);
@@ -601,7 +601,7 @@ describe("Room", function() {
});
const resetTimelineTests = function(timelineSupport) {
let events = null;
let events: MatrixEvent[];
beforeEach(function() {
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
@@ -722,13 +722,13 @@ describe("Room", function() {
it("should handle events in the same timeline", function() {
room.addLiveEvents(events);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
.toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!,
events[1].getId()))
.toBeGreaterThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[1].getId()))
.toEqual(0);
});
@@ -741,10 +741,10 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
.toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[0].getId()))
.toBeGreaterThan(0);
});
@@ -755,10 +755,10 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
.toBe(null);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[0].getId()))
.toBe(null);
});
@@ -767,13 +767,13 @@ describe("Room", function() {
room.addLiveEvents(events);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), "xxx"))
.compareEventOrdering(events[0].getId()!, "xxx"))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId()))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), events[0].getId()))
.compareEventOrdering(events[0].getId()!, events[0].getId()))
.toBe(0);
});
});
@@ -1228,7 +1228,7 @@ describe("Room", function() {
it("should store the receipt so it can be obtained via getReceiptsForEvent", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
]));
expect(room.getReceiptsForEvent(eventToAck)).toEqual([{
type: "m.read",
@@ -1247,7 +1247,7 @@ describe("Room", function() {
const ts = 13787898424;
const receiptEvent = mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
]);
room.addReceipt(receiptEvent);
@@ -1261,11 +1261,11 @@ describe("Room", function() {
});
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
]));
const ts2 = 13787899999;
room.addReceipt(mkReceipt(roomId, [
mkRecord(nextEventToAck.getId(), "m.read", userB, ts2),
mkRecord(nextEventToAck.getId()!, "m.read", userB, ts2),
]));
expect(room.getReceiptsForEvent(eventToAck)).toEqual([]);
expect(room.getReceiptsForEvent(nextEventToAck)).toEqual([{
@@ -1280,9 +1280,9 @@ describe("Room", function() {
it("should persist multiple receipts for a single event ID", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId(), "m.read", userC, ts),
mkRecord(eventToAck.getId(), "m.read", userD, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userC, ts),
mkRecord(eventToAck.getId()!, "m.read", userD, ts),
]));
expect(room.getUsersReadUpTo(eventToAck)).toEqual(
[userB, userC, userD],
@@ -1300,9 +1300,9 @@ describe("Room", function() {
});
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventTwo.getId(), "m.read", userC, ts),
mkRecord(eventThree.getId(), "m.read", userD, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
mkRecord(eventTwo.getId()!, "m.read", userC, ts),
mkRecord(eventThree.getId()!, "m.read", userD, ts),
]));
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
expect(room.getUsersReadUpTo(eventTwo)).toEqual([userC]);
@@ -1311,9 +1311,9 @@ describe("Room", function() {
it("should persist multiple receipts for a single user ID", function() {
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.delivered", userB, 13787898424),
mkRecord(eventToAck.getId(), "m.read", userB, 22222222),
mkRecord(eventToAck.getId(), "m.seen", userB, 33333333),
mkRecord(eventToAck.getId()!, "m.delivered", userB, 13787898424),
mkRecord(eventToAck.getId()!, "m.read", userB, 22222222),
mkRecord(eventToAck.getId()!, "m.seen", userB, 33333333),
]));
expect(room.getReceiptsForEvent(eventToAck)).toEqual([
{
@@ -1361,19 +1361,19 @@ describe("Room", function() {
// check it initialises correctly
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[0].getId(), "m.read", userB, ts),
mkRecord(events[0].getId()!, "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
// 2>0, so it should move forward
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[2].getId(), "m.read", userB, ts),
mkRecord(events[2].getId()!, "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
// 1<2, so it should stay put
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[1].getId(), "m.read", userB, ts),
mkRecord(events[1].getId()!, "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
});
@@ -1399,13 +1399,13 @@ describe("Room", function() {
// check it initialises correctly
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[0].getId(), "m.read", userB, ts),
mkRecord(events[0].getId()!, "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
// 2>0, so it should move forward
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[2].getId(), "m.read", userB, ts),
mkRecord(events[2].getId()!, "m.read", userB, ts),
]), true);
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
expect(room.getReceiptsForEvent(events[2])).toEqual([
@@ -1414,7 +1414,7 @@ describe("Room", function() {
// 1<2, so it should stay put
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[1].getId(), "m.read", userB, ts),
mkRecord(events[1].getId()!, "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
expect(room.getEventReadUpTo(userB, true)).toEqual(events[1].getId());
@@ -1428,7 +1428,7 @@ describe("Room", function() {
it("should return user IDs read up to the given event", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
]));
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
});
@@ -1438,9 +1438,9 @@ describe("Room", function() {
it("should acknowledge if an event has been read", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
mkRecord(eventToAck.getId()!, "m.read", userB, ts),
]));
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
expect(room.hasUserReadEvent(userB, eventToAck.getId()!)).toEqual(true);
});
it("return false for an unknown event", function() {
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
@@ -1556,7 +1556,7 @@ describe("Room", function() {
user: userA,
type: EventType.RoomRedaction,
content: {},
redacts: eventA.getId(),
redacts: eventA.getId()!,
event: true,
});
redactA.status = EventStatus.SENDING;
@@ -1609,7 +1609,7 @@ describe("Room", function() {
});
it("should remove cancelled events from the timeline", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, null!, userA);
const eventA = utils.mkMessage({
room: roomId, user: userA, event: true,
});
@@ -1643,7 +1643,7 @@ describe("Room", function() {
});
describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse = null) {
function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) {
return {
getEventMapper: function() {
// events should already be MatrixEvents
@@ -1664,7 +1664,7 @@ describe("Room", function() {
}),
store: {
storageResponse,
storedMembers: null,
storedMembers: [] as IStateEventWithRoomId[] | null,
getOutOfBandMembers: function() {
if (this.storageResponse instanceof Error) {
return Promise.reject(this.storageResponse);
@@ -1693,11 +1693,11 @@ describe("Room", function() {
it("should load members from server on first call", async function() {
const client = createClientMock([memberEvent]);
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
const memberA = room.getMember("@user_a:bar")!;
expect(memberA.name).toEqual("User A");
const storedMembers = client.store.storedMembers;
const storedMembers = client.store.storedMembers!;
expect(storedMembers.length).toEqual(1);
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
});
@@ -1711,17 +1711,17 @@ describe("Room", function() {
name: "Ms A",
});
const client = createClientMock([memberEvent2], [memberEvent]);
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
const memberA = room.getMember("@user_a:bar")!;
expect(memberA.name).toEqual("User A");
});
it("should allow retry on error", async function() {
const client = createClientMock(new Error("server says no"));
const room = new Room(roomId, client as any, null, { lazyLoadMembers: true });
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
let hasThrown = false;
try {
await room.loadMembersIfNeeded();
@@ -1732,7 +1732,7 @@ describe("Room", function() {
client.members.mockReturnValue({ chunk: [memberEvent] });
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
const memberA = room.getMember("@user_a:bar")!;
expect(memberA.name).toEqual("User A");
});
});
@@ -1740,27 +1740,68 @@ describe("Room", function() {
describe("getMyMembership", function() {
it("should return synced membership if membership isn't available yet",
function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, null!, userA);
room.updateMyMembership(JoinRule.Invite);
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
});
it("should emit a Room.myMembership event on a change",
function() {
const room = new Room(roomId, null, userA);
const events = [];
room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => {
events.push({ membership, oldMembership });
});
room.updateMyMembership(JoinRule.Invite);
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined });
events.splice(0); //clear
room.updateMyMembership(JoinRule.Invite);
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
it("should emit a Room.myMembership event on a change", function() {
const room = new Room(roomId, null!, userA);
const events: {
membership: string;
oldMembership?: string;
}[] = [];
room.on(RoomEvent.MyMembership, (_room, membership, oldMembership) => {
events.push({ membership, oldMembership });
});
room.updateMyMembership(JoinRule.Invite);
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined });
events.splice(0); //clear
room.updateMyMembership(JoinRule.Invite);
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
});
});
describe("getDMInviter", () => {
it("should delegate to RoomMember::getDMInviter if available", () => {
const room = new Room(roomId, null!, userA);
room.currentState.markOutOfBandMembersStarted();
room.currentState.setOutOfBandMembers([
new MatrixEvent({
type: EventType.RoomMember,
state_key: userA,
sender: userB,
content: {
membership: "invite",
is_direct: true,
},
}),
]);
expect(room.getDMInviter()).toBe(userB);
});
it("should fall back to summary heroes and return the first one", () => {
const room = new Room(roomId, null!, userA);
room.updateMyMembership("invite");
room.setSummary({
"m.heroes": [userA, userC],
"m.joined_member_count": 1,
"m.invited_member_count": 1,
});
expect(room.getDMInviter()).toBe(userC);
});
it("should return undefined if we're not joined or invited to the room", () => {
const room = new Room(roomId, null!, userA);
expect(room.getDMInviter()).toBeUndefined();
room.updateMyMembership("leave");
expect(room.getDMInviter()).toBeUndefined();
});
});
describe("guessDMUserId", function() {
@@ -1789,6 +1830,36 @@ describe("Room", function() {
});
});
describe("getAvatarFallbackMember", () => {
it("should should return undefined if the room isn't a 1:1", () => {
const room = new Room(roomId, null!, userA);
room.currentState.setJoinedMemberCount(2);
room.currentState.setInvitedMemberCount(1);
expect(room.getAvatarFallbackMember()).toBeUndefined();
});
it("should use summary heroes member if 1:1", () => {
const room = new Room(roomId, null!, userA);
room.currentState.markOutOfBandMembersStarted();
room.currentState.setOutOfBandMembers([
new MatrixEvent({
type: EventType.RoomMember,
state_key: userD,
sender: userD,
content: {
membership: "join",
},
}),
]);
room.setSummary({
"m.heroes": [userA, userD],
"m.joined_member_count": 1,
"m.invited_member_count": 1,
});
expect(room.getAvatarFallbackMember()?.userId).toBe(userD);
});
});
describe("maySendMessage", function() {
it("should return false if synced membership not join", function() {
const room = new Room(roomId, { isRoomEncrypted: () => false } as any, userA);
@@ -2118,7 +2189,7 @@ describe("Room", function() {
},
});
expect(() => room.createThread(rootEvent.getId(), rootEvent, [])).not.toThrow();
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow();
});
it("creating thread from edited event should not conflate old versions of the event", () => {
@@ -2132,6 +2203,7 @@ describe("Room", function() {
it("Edits update the lastReply event", async () => {
room.client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const randomMessage = mkMessage();
const threadRoot = mkMessage();
@@ -2145,7 +2217,7 @@ describe("Room", function() {
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
[THREAD_RELATION_TYPE.name]: {
latest_event: threadResponse.event,
count: 2,
current_user_participated: true,
@@ -2157,11 +2229,29 @@ describe("Room", function() {
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread.replyToEvent).toBe(threadResponse);
expect(thread.replyToEvent.event).toEqual(threadResponse.event);
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
prom = emitPromise(thread, ThreadEvent.Update);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
[THREAD_RELATION_TYPE.name]: {
latest_event: {
...threadResponse.event,
content: threadResponseEdit.event.content,
},
count: 2,
current_user_participated: true,
},
},
},
});
prom = emitPromise(room, ThreadEvent.Update);
room.addLiveEvents([threadResponseEdit]);
await prom;
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
@@ -2169,6 +2259,7 @@ describe("Room", function() {
it("Redactions to thread responses decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2181,7 +2272,7 @@ describe("Room", function() {
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
[THREAD_RELATION_TYPE.name]: {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
@@ -2193,10 +2284,36 @@ describe("Room", function() {
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
thread.timelineSet.addEventToTimeline(
threadResponse1,
thread.liveTimeline,
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
);
thread.timelineSet.addEventToTimeline(
threadResponse2,
thread.liveTimeline,
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
[THREAD_RELATION_TYPE.name]: {
latest_event: threadResponse2.event,
count: 1,
current_user_participated: true,
},
},
},
});
prom = emitPromise(thread, ThreadEvent.Update);
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
@@ -2207,6 +2324,7 @@ describe("Room", function() {
it("Redactions to reactions in threads do not decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2220,7 +2338,7 @@ describe("Room", function() {
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
[THREAD_RELATION_TYPE.name]: {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
@@ -2232,6 +2350,7 @@ describe("Room", function() {
const prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
@@ -2244,6 +2363,7 @@ describe("Room", function() {
it("should not decrement the length when the thread root is redacted", async () => {
room.client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2257,7 +2377,7 @@ describe("Room", function() {
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
[THREAD_RELATION_TYPE.name]: {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
@@ -2269,6 +2389,7 @@ describe("Room", function() {
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
@@ -2282,6 +2403,18 @@ describe("Room", function() {
it("Redacting the lastEvent finds a new lastEvent", async () => {
room.client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
room.client.createThreadListMessagesRequest = () => Promise.resolve({
start: null,
end: null,
chunk: [],
state: [],
});
await room.createThreadsTimelineSets();
await room.fetchRoomThreads();
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
@@ -2306,21 +2439,53 @@ describe("Room", function() {
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse1.event,
count: 1,
current_user_participated: true,
},
},
},
});
prom = emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2);
room.addLiveEvents([threadResponse2Redaction]);
await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
prom = emitPromise(room, ThreadEvent.Update);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadRoot.event,
count: 0,
current_user_participated: true,
},
},
},
});
prom = emitPromise(room, ThreadEvent.Delete);
const prom2 = emitPromise(room, RoomEvent.Timeline);
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await prom;
await prom2;
expect(thread).toHaveLength(0);
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
});
@@ -2329,6 +2494,7 @@ describe("Room", function() {
describe("eventShouldLiveIn", () => {
const client = new TestClient(userA).client;
client.supportsExperimentalThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const room = new Room(roomId, client, userA);
it("thread root and its relations&redactions should be in both", () => {
@@ -2339,7 +2505,7 @@ describe("Room", function() {
const threadReaction2 = mkReaction(threadRoot);
const threadReaction2Redaction = mkRedaction(threadReaction2);
const roots = new Set([threadRoot.getId()]);
const roots = new Set([threadRoot.getId()!]);
const events = [
randomMessage,
threadRoot,
@@ -2377,7 +2543,7 @@ describe("Room", function() {
const threadReaction2 = mkReaction(threadResponse1);
const threadReaction2Redaction = mkRedaction(threadReaction2);
const roots = new Set([threadRoot.getId()]);
const roots = new Set([threadRoot.getId()!]);
const events = [threadRoot, threadResponse1, threadReaction1, threadReaction2, threadReaction2Redaction];
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeFalsy();
@@ -2399,7 +2565,7 @@ describe("Room", function() {
const reaction2 = mkReaction(reply1);
const reaction2Redaction = mkRedaction(reply1);
const roots = new Set([threadRoot.getId()]);
const roots = new Set([threadRoot.getId()!]);
const events = [
threadRoot,
threadResponse1,
@@ -2425,7 +2591,7 @@ describe("Room", function() {
const reply1 = mkReply(threadRoot);
const reply2 = mkReply(reply1);
const roots = new Set([threadRoot.getId()]);
const roots = new Set([threadRoot.getId()!]);
const events = [
threadRoot,
threadResponse1,
@@ -2455,28 +2621,28 @@ describe("Room", function() {
room.addLiveEvents(events);
const thread = threadRoot.getThread();
const thread = threadRoot.getThread()!;
expect(thread.rootEvent).toBe(threadRoot);
const rootRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadRoot.getId(),
threadRoot.getId()!,
RelationType.Annotation,
EventType.Reaction,
).getSortedAnnotationsByKey();
)!.getSortedAnnotationsByKey();
expect(rootRelations).toHaveLength(1);
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key);
expect(rootRelations[0][1].size).toEqual(1);
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
expect(rootRelations![0][0]).toEqual(rootReaction.getRelation()!.key);
expect(rootRelations![0][1].size).toEqual(1);
expect(rootRelations![0][1].has(rootReaction)).toBeTruthy();
const responseRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadResponse.getId(),
threadResponse.getId()!,
RelationType.Annotation,
EventType.Reaction,
).getSortedAnnotationsByKey();
)!.getSortedAnnotationsByKey();
expect(responseRelations).toHaveLength(1);
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key);
expect(responseRelations[0][1].size).toEqual(1);
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy();
expect(responseRelations![0][0]).toEqual(threadReaction.getRelation()!.key);
expect(responseRelations![0][1].size).toEqual(1);
expect(responseRelations![0][1].has(threadReaction)).toBeTruthy();
});
});
@@ -2642,6 +2808,29 @@ describe("Room", function() {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
});
it("partially resets room notifications", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
room.resetThreadUnreadNotificationCount(["123"]);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(666);
expect(room.getThreadUnreadNotificationCount("456", NotificationCountType.Highlight)).toBe(0);
});
it("emits event on notifications reset", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
room.resetThreadUnreadNotificationCount();
expect(cb).toHaveBeenLastCalledWith();
});
});
describe("hasThreadUnreadNotification", () => {
@@ -2734,7 +2923,18 @@ describe("Room", function() {
expect(pendingEvents[1].isBeingDecrypted()).toBeFalsy();
expect(pendingEvents[1].isEncrypted()).toBeTruthy();
for (const ev of pendingEvents) {
expect(room.getPendingEvent(ev.getId())).toBe(ev);
expect(room.getPendingEvent(ev.getId()!)).toBe(ev);
}
});
describe("getBlacklistUnverifiedDevices", () => {
it("defaults to null", () => {
expect(room.getBlacklistUnverifiedDevices()).toBeNull();
});
it("is updated by setBlacklistUnverifiedDevices", () => {
room.setBlacklistUnverifiedDevices(false);
expect(room.getBlacklistUnverifiedDevices()).toBe(false);
});
});
});
+4 -4
View File
@@ -160,10 +160,10 @@ describe("MatrixScheduler", function() {
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
const buckets = {};
buckets[eventA.getId()] = "queue_A";
buckets[eventD.getId()] = "queue_A";
buckets[eventB.getId()] = "queue_B";
buckets[eventC.getId()] = "queue_B";
buckets[eventA.getId()!] = "queue_A";
buckets[eventD.getId()!] = "queue_A";
buckets[eventB.getId()!] = "queue_B";
buckets[eventC.getId()!] = "queue_B";
retryFn = function() {
return 0;
+2 -2
View File
@@ -37,7 +37,7 @@ const mockClient = {
function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline {
const room = new Room(ROOM_ID, mockClient, USER_ID);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet);
@@ -170,7 +170,7 @@ describe("TimelineWindow", function() {
beforeEach(() => {
jest.clearAllMocks();
mockClient.getEventTimeline.mockResolvedValue(undefined);
mockClient.paginateEventTimeline.mockReturnValue(undefined);
mockClient.paginateEventTimeline.mockResolvedValue(false);
});
describe("load", function() {
+40 -25
View File
@@ -26,17 +26,19 @@ import {
MockRTCPeerConnection,
} from "../../test-utils/webrtc";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { EventType, MatrixClient } from "../../../src";
import { MediaHandler } from "../../../src/webrtc/mediaHandler";
const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise<void> => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush("");
await client.httpBackend!.flush("");
await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
};
describe('Call', function() {
let client;
let client: TestClient;
let call;
let prevNavigator;
let prevDocument;
@@ -71,10 +73,10 @@ describe('Call', function() {
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.client.mediaHandler = new MockMediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
client.client.sendEvent = (() => {}) as unknown as MatrixClient["sendEvent"];
client.client["mediaHandler"] = new MockMediaHandler as unknown as MediaHandler;
client.client.getMediaHandler = () => client.client["mediaHandler"]!;
client.httpBackend!.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
roomId: '!foo:bar',
@@ -237,7 +239,7 @@ describe('Call', function() {
expect(identChangedCallback).toHaveBeenCalled();
const ident = call.getRemoteAssertedIdentity();
const ident = call.getRemoteAssertedIdentity()!;
expect(ident.id).toEqual("@steve:example.com");
expect(ident.displayName).toEqual("Steve Gibbons");
@@ -306,19 +308,19 @@ describe('Call', function() {
});
it("should fallback to answering with no video", async () => {
await client.httpBackend.flush();
await client.httpBackend!.flush(undefined);
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
client.client["mediaHandler"].getUserMediaStream = jest.fn().mockRejectedValue("reject");
await call.answer(true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
});
it("should handle mid-call device changes", async () => {
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
client.client["mediaHandler"].getUserMediaStream = jest.fn().mockReturnValue(
new MockMediaStream(
"stream", [
new MockMediaStreamTrack("audio_track", "audio"),
@@ -424,7 +426,7 @@ describe('Call', function() {
it("should choose opponent member", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await client.httpBackend!.flush(undefined);
await callPromise;
const opponentMember = {
@@ -480,7 +482,7 @@ describe('Call', function() {
it("should correctly generate local SDPStreamMetadata", async () => {
const callPromise = call.placeCallWithCallFeeds([new CallFeed({
client,
client: client.client,
// @ts-ignore Mock
stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]),
roomId: call.roomId,
@@ -489,7 +491,7 @@ describe('Call', function() {
audioMuted: false,
videoMuted: false,
})]);
await client.httpBackend.flush();
await client.httpBackend!.flush(undefined);
await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
@@ -521,7 +523,7 @@ describe('Call', function() {
const callPromise = call.placeCallWithCallFeeds([
new CallFeed({
client,
client: client.client,
userId: client.getUserId(),
// @ts-ignore Mock
stream: localUsermediaStream,
@@ -531,7 +533,7 @@ describe('Call', function() {
videoMuted: false,
}),
new CallFeed({
client,
client: client.client,
userId: client.getUserId(),
// @ts-ignore Mock
stream: localScreensharingStream,
@@ -541,7 +543,7 @@ describe('Call', function() {
videoMuted: false,
}),
]);
await client.httpBackend.flush();
await client.httpBackend!.flush(undefined);
await callPromise;
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
@@ -586,14 +588,14 @@ describe('Call', function() {
getLocalAge: () => null,
});
call.feeds.push(new CallFeed({
client,
client: client.client,
userId: "remote_user_id",
// @ts-ignore Mock
stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]),
id: "remote_feed_id",
purpose: SDPStreamMetadataPurpose.Usermedia,
}));
await client.httpBackend.flush();
await client.httpBackend!.flush(undefined);
await callPromise;
const callHangupCallback = jest.fn();
@@ -664,10 +666,10 @@ describe('Call', function() {
});
it("should return false if window or document are undefined", () => {
global.window = undefined;
global.window = undefined!;
expect(supportsMatrixCall()).toBe(false);
global.window = prevWindow;
global.document = undefined;
global.document = undefined!;
expect(supportsMatrixCall()).toBe(false);
});
@@ -685,9 +687,9 @@ describe('Call', function() {
it("should return false if RTCPeerConnection & RTCSessionDescription " +
"& RTCIceCandidate & mediaDevices are unavailable",
() => {
global.window.RTCPeerConnection = undefined;
global.window.RTCSessionDescription = undefined;
global.window.RTCIceCandidate = undefined;
global.window.RTCPeerConnection = undefined!;
global.window.RTCSessionDescription = undefined!;
global.window.RTCIceCandidate = undefined!;
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
global.navigator.mediaDevices = undefined;
expect(supportsMatrixCall()).toBe(false);
@@ -752,4 +754,17 @@ describe('Call', function() {
expect(call.pushLocalFeed).toHaveBeenCalled();
});
});
describe("transferToCall", () => {
it("should send the required events", async () => {
const targetCall = new MatrixCall({ client: client.client });
const sendEvent = jest.spyOn(client.client, "sendEvent");
await call.transferToCall(targetCall);
const newCallId = (sendEvent.mock.calls[0][2] as any)!.await_call;
expect(sendEvent).toHaveBeenCalledWith(call.roomId, EventType.CallReplaces, expect.objectContaining({
create_call: newCallId,
}));
});
});
});
+1 -1
View File
@@ -58,7 +58,7 @@ describe("callEventHandler", () => {
client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted);
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
client.emit(ClientEvent.Sync, SyncState.Syncing);
client.emit(ClientEvent.Sync, SyncState.Syncing, null);
expect(incomingCallEmitted).not.toHaveBeenCalled();
});
+13 -1
View File
@@ -65,6 +65,8 @@ export enum ConditionKind {
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
CallStarted = "call_started",
CallStartedPrefix = "org.matrix.msc3914.call_started",
}
export interface IPushRuleCondition<N extends ConditionKind | string> {
@@ -90,12 +92,22 @@ export interface ISenderNotificationPermissionCondition
key: string;
}
export interface ICallStartedCondition extends IPushRuleCondition<ConditionKind.CallStarted> {
// no additional fields
}
export interface ICallStartedPrefixCondition extends IPushRuleCondition<ConditionKind.CallStartedPrefix> {
// no additional fields
}
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition = IEventMatchCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition;
| ISenderNotificationPermissionCondition
| ICallStartedCondition
| ICallStartedPrefixCondition;
export enum PushRuleKind {
Override = "override",
+4
View File
@@ -59,6 +59,10 @@ export enum EventType {
KeyVerificationCancel = "m.key.verification.cancel",
KeyVerificationMac = "m.key.verification.mac",
KeyVerificationDone = "m.key.verification.done",
KeyVerificationKey = "m.key.verification.key",
KeyVerificationAccept = "m.key.verification.accept",
// XXX this event is not yet supported by js-sdk
KeyVerificationReady = "m.key.verification.ready",
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
RoomMessageFeedback = "m.room.message.feedback",
Reaction = "m.reaction",
+10 -7
View File
@@ -23,7 +23,10 @@ import { Optional } from "matrix-events-sdk/lib/types";
export class NamespacedValue<S extends string, U extends string> {
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
// Goal is to to have developers explicitly say there is no stable value (if applicable).
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) {
public constructor(stable: S, unstable: U);
public constructor(stable: S, unstable?: U);
public constructor(stable: null | undefined, unstable: U);
public constructor(public readonly stable?: S | null, public readonly unstable?: U) {
if (!this.unstable && !this.stable) {
throw new Error("One of stable or unstable values must be supplied");
}
@@ -33,10 +36,10 @@ export class NamespacedValue<S extends string, U extends string> {
if (this.stable) {
return this.stable;
}
return this.unstable;
return this.unstable!;
}
public get altName(): U | S | null {
public get altName(): U | S | null | undefined {
if (!this.stable) {
return null;
}
@@ -57,7 +60,7 @@ export class NamespacedValue<S extends string, U extends string> {
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): Optional<T> {
let val: T;
let val: T | undefined = undefined;
if (this.name) {
val = obj?.[this.name];
}
@@ -91,7 +94,7 @@ export class ServerControlledNamespacedValue<S extends string, U extends string>
if (this.stable && !this.preferUnstable) {
return this.stable;
}
return this.unstable;
return this.unstable!;
}
}
@@ -109,10 +112,10 @@ export class UnstableValue<S extends string, U extends string> extends Namespace
}
public get name(): U {
return this.unstable;
return this.unstable!;
}
public get altName(): S {
return this.stable;
return this.stable!;
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ export class ReEmitter {
if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
for (const eventName of eventNames) {
source.off(eventName, reEmittersByEvent.get(eventName));
source.off(eventName, reEmittersByEvent.get(eventName)!);
reEmittersByEvent.delete(eventName);
}
+13 -7
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { logger } from "./logger";
import { MatrixClient } from "./matrix";
import { MatrixError, MatrixClient } from "./matrix";
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
import { MatrixScheduler } from "./scheduler";
@@ -48,14 +48,18 @@ export class ToDeviceMessageQueue {
public async queueBatch(batch: ToDeviceBatch): Promise<void> {
const batches: ToDeviceBatchWithTxnId[] = [];
for (let i = 0; i < batch.batch.length; i += MAX_BATCH_SIZE) {
batches.push({
const batchWithTxnId = {
eventType: batch.eventType,
batch: batch.batch.slice(i, i + MAX_BATCH_SIZE),
txnId: this.client.makeTxnId(),
});
};
batches.push(batchWithTxnId);
const recips = batchWithTxnId.batch.map((msg) => `${msg.userId}:${msg.deviceId}`);
logger.info(`Created batch of to-device messages with txn id ${batchWithTxnId.txnId} for ${recips}`);
}
await this.client.store.saveToDeviceBatches(batches);
logger.info(`Enqueued to-device messages with txn ids ${batches.map((batch) => batch.txnId)}`);
this.sendQueue();
}
@@ -68,7 +72,7 @@ export class ToDeviceMessageQueue {
logger.debug("Attempting to send queued to-device messages");
this.sending = true;
let headBatch: IndexedToDeviceBatch;
let headBatch: IndexedToDeviceBatch | null;
try {
while (this.running) {
headBatch = await this.client.store.getOldestToDeviceBatch();
@@ -86,11 +90,11 @@ export class ToDeviceMessageQueue {
++this.retryAttempts;
// eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line new-cap
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e);
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, <MatrixError>e);
if (retryDelay === -1) {
// the scheduler function doesn't differentiate between fatal errors and just getting
// bored and giving up for now
if (Math.floor(e.httpStatus / 100) === 4) {
if (Math.floor((<MatrixError>e).httpStatus! / 100) === 4) {
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
await this.client.store.removeToDeviceBatch(headBatch!.id);
} else {
@@ -118,7 +122,9 @@ export class ToDeviceMessageQueue {
contentMap[item.userId][item.deviceId] = item.payload;
}
logger.info(`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id}`);
logger.info(
`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`,
);
await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId);
}
+1 -1
View File
@@ -347,7 +347,7 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
+568 -355
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -139,8 +139,7 @@ export const getTextForLocationEvent = (
/**
* Generates the content for a Location event
* @param uri a geo:// uri for the location
* @param timestamp the timestamp when the location was correct (milliseconds since
* the UNIX epoch)
* @param timestamp the timestamp when the location was correct (milliseconds since the UNIX epoch)
* @param description the (optional) label for this location on the map
* @param assetType the (optional) asset type of this location e.g. "m.self"
* @param text optional. A text for the location
@@ -150,7 +149,7 @@ export const makeLocationContent = (
// to avoid a breaking change
text: string | undefined,
uri: string,
timestamp?: number,
timestamp: number,
description?: string,
assetType?: LocationAssetType,
): LegacyLocationEventContent & MLocationEventContent => {
+28 -27
View File
@@ -44,8 +44,8 @@ function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
}
export interface ICacheCallbacks {
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array | null>;
storeCrossSigningKeyCache?(type: string, key?: Uint8Array): Promise<void>;
}
export interface ICrossSigningInfo {
@@ -114,10 +114,10 @@ export class CrossSigningInfo {
}
if (expectedPubkey === undefined) {
expectedPubkey = this.getId(type);
expectedPubkey = this.getId(type)!;
}
function validateKey(key: Uint8Array): [string, PkSigning] {
function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
if (!key) return;
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key);
@@ -127,7 +127,7 @@ export class CrossSigningInfo {
signing.free();
}
let privkey;
let privkey: Uint8Array | null = null;
if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
}
@@ -141,7 +141,7 @@ export class CrossSigningInfo {
const result = validateKey(privkey);
if (result) {
if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!);
}
return result;
}
@@ -169,7 +169,9 @@ export class CrossSigningInfo {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
public async isStoredInSecretStorage(
secretStorage: SecretStorage<MatrixClient | undefined>,
): Promise<Record<string, object> | null> {
// check what SSSS keys have encrypted the master key (if any)
const stored = await secretStorage.isStored("m.cross_signing.master") || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
@@ -196,7 +198,7 @@ export class CrossSigningInfo {
*/
public static async storeInSecretStorage(
keys: Map<string, Uint8Array>,
secretStorage: SecretStorage,
secretStorage: SecretStorage<undefined>,
): Promise<void> {
for (const [type, privateKey] of keys) {
const encodedKey = encodeBase64(privateKey);
@@ -213,7 +215,7 @@ export class CrossSigningInfo {
* @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key
*/
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> {
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) {
return null;
@@ -233,7 +235,7 @@ export class CrossSigningInfo {
if (!cacheCallbacks) return false;
const types = type ? [type] : ["master", "self_signing", "user_signing"];
for (const t of types) {
if (!await cacheCallbacks.getCrossSigningKeyCache(t)) {
if (!await cacheCallbacks.getCrossSigningKeyCache?.(t)) {
return false;
}
}
@@ -250,7 +252,7 @@ export class CrossSigningInfo {
const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) {
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type);
if (!privKey) {
continue;
}
@@ -268,7 +270,7 @@ export class CrossSigningInfo {
*
* @return {string} the ID
*/
public getId(type = "master"): string {
public getId(type = "master"): string | null {
if (!this.keys[type]) return null;
const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo);
@@ -433,10 +435,9 @@ export class CrossSigningInfo {
// if everything checks out, then save the keys
if (keys.master) {
this.keys.master = keys.master;
// if the master key is set, then the old self-signing and
// user-signing keys are obsolete
this.keys.self_signing = null;
this.keys.user_signing = null;
// if the master key is set, then the old self-signing and user-signing keys are obsolete
delete this.keys["self_signing"];
delete this.keys["user_signing"];
}
if (keys.self_signing) {
this.keys.self_signing = keys.self_signing;
@@ -469,7 +470,7 @@ export class CrossSigningInfo {
}
}
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey> {
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user");
return;
@@ -477,7 +478,7 @@ export class CrossSigningInfo {
return this.signObject(key.keys.master, "user_signing");
}
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey> {
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey | undefined> {
if (userId !== this.userId) {
throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`,
@@ -521,9 +522,9 @@ export class CrossSigningInfo {
return new UserTrustLevel(false, false, userCrossSigning.firstUse);
}
let userTrusted;
let userTrusted: boolean;
const userMaster = userCrossSigning.keys.master;
const uskId = this.getId('user_signing');
const uskId = this.getId('user_signing')!;
try {
pkVerify(userMaster, uskId, this.userId);
userTrusted = true;
@@ -567,7 +568,7 @@ export class CrossSigningInfo {
const deviceObj = deviceToObject(device, userCrossSigning.userId);
try {
// if we can verify the user's SSK from their master key...
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId);
// ...and this device's key from their SSK...
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
// ...then we trust this device as much as far as we trust the user
@@ -723,7 +724,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
},
storeCrossSigningKeyCache: async function(
type: keyof SecretStorePrivateKeys,
key: Uint8Array,
key?: Uint8Array,
): Promise<void> {
if (!(key instanceof Uint8Array)) {
throw new Error(
@@ -752,7 +753,7 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning],
* @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified
*/
export function requestKeysDuringVerification(
export async function requestKeysDuringVerification(
baseApis: MatrixClient,
userId: string,
deviceId: string,
@@ -766,7 +767,7 @@ export function requestKeysDuringVerification(
// it. We return here in order to test.
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
const client = baseApis;
const original = client.crypto.crossSigningInfo;
const original = client.crypto!.crossSigningInfo;
// We already have all of the infrastructure we need to validate and
// cache cross-signing keys, so instead of replicating that, here we set
@@ -801,7 +802,7 @@ export function requestKeysDuringVerification(
// also request and cache the key backup key
const backupKeyPromise = (async () => {
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret(
@@ -811,13 +812,13 @@ export function requestKeysDuringVerification(
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
await client.crypto.storeSessionBackupPrivateKey(
await client.crypto!.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => {
logger.info("Backup restored.");
});
}
+42 -45
View File
@@ -26,7 +26,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { IDownloadKeyResult, MatrixClient } from "../client";
import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client";
import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base";
import { TypedEventEmitter } from "../models/typed-event-emitter";
@@ -81,24 +81,24 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
// The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the
// moment represented by the snapshot in the db.
private syncToken: string = null;
private syncToken: string | null = null;
private keyDownloadsInProgressByUser: { [userId: string]: Promise<void> } = {};
private keyDownloadsInProgressByUser = new Map<string, Promise<void>>();
// Set whenever changes are made other than setting the sync token
private dirty = false;
// Promise resolved when device data is saved
private savePromise: Promise<boolean> = null;
private savePromise: Promise<boolean> | null = null;
// Function that resolves the save promise
private resolveSavePromise: (saved: boolean) => void = null;
private resolveSavePromise: ((saved: boolean) => void) | null = null;
// The time the save is scheduled for
private savePromiseTime: number = null;
private savePromiseTime: number | null = null;
// The timer used to delay the save
private saveTimer: ReturnType<typeof setTimeout> = null;
private saveTimer: ReturnType<typeof setTimeout> | null = null;
// True if we have fetched data from the server or loaded a non-empty
// set of device data from the store
private hasFetched: boolean = null;
private hasFetched: boolean | null = null;
private readonly serialiser: DeviceListUpdateSerialiser;
@@ -127,7 +127,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
deviceData.crossSigningInfo || {} : {};
this.deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this.syncToken = deviceData ? deviceData.syncToken : null;
this.syncToken = deviceData?.syncToken ?? null;
this.userByIdentityKey = {};
for (const user of Object.keys(this.devices)) {
const userDevices = this.devices[user];
@@ -181,7 +181,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
if (this.savePromiseTime && targetTime < this.savePromiseTime) {
// There's a save scheduled but for after we would like: cancel
// it & schedule one for the time we want
clearTimeout(this.saveTimer);
clearTimeout(this.saveTimer!);
this.saveTimer = null;
this.savePromiseTime = null;
// (but keep the save promise since whatever called save before
@@ -216,13 +216,13 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
devices: this.devices,
crossSigningInfo: this.crossSigningInfo,
trackingStatus: this.deviceTrackingStatus,
syncToken: this.syncToken,
syncToken: this.syncToken ?? undefined,
}, txn);
},
).then(() => {
// The device list is considered dirty until the write completes.
this.dirty = false;
resolveSavePromise(true);
resolveSavePromise?.(true);
}, err => {
logger.error('Failed to save device tracking data', this.syncToken);
logger.error(err);
@@ -230,7 +230,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
}, delay);
}
return savePromise;
return savePromise!;
}
/**
@@ -238,7 +238,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
*
* @return {string} The sync token
*/
public getSyncToken(): string {
public getSyncToken(): string | null {
return this.syncToken;
}
@@ -252,7 +252,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
*
* @param {string} st The sync token
*/
public setSyncToken(st: string): void {
public setSyncToken(st: string | null): void {
this.syncToken = st;
}
@@ -272,14 +272,14 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
userIds.forEach((u) => {
const trackingStatus = this.deviceTrackingStatus[u];
if (this.keyDownloadsInProgressByUser[u]) {
if (this.keyDownloadsInProgressByUser.has(u)) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
logger.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
promises.push(this.keyDownloadsInProgressByUser[u]);
promises.push(this.keyDownloadsInProgressByUser.get(u)!);
} else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
usersToDownload.push(u);
}
@@ -341,7 +341,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
if (!devs) {
return null;
}
const res = [];
const res: DeviceInfo[] = [];
for (const deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
@@ -362,7 +362,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
return this.devices[userId];
}
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo {
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
if (!this.crossSigningInfo[userId]) return null;
return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
@@ -382,9 +382,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device
*/
public getStoredDevice(userId: string, deviceId: string): DeviceInfo {
public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
const devs = this.devices[userId];
if (!devs || !devs[deviceId]) {
if (!devs?.[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
@@ -398,11 +398,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
*
* @return {string} user ID
*/
public getUserByIdentityKey(algorithm: string, senderKey: string): string {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
// we only deal in olm keys
return null;
}
@@ -557,7 +554,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
public refreshOutdatedDeviceLists(): Promise<void> {
this.saveIfDirty();
const usersToDownload = [];
const usersToDownload: string[] = [];
for (const userId of Object.keys(this.deviceTrackingStatus)) {
const stat = this.deviceTrackingStatus[userId];
if (stat == TrackingStatus.PendingDownload) {
@@ -617,7 +614,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
return Promise.resolve();
}
const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => {
const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(() => {
finished(true);
}, (e) => {
logger.error(
@@ -628,7 +625,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
});
users.forEach((u) => {
this.keyDownloadsInProgressByUser[u] = prom;
this.keyDownloadsInProgressByUser.set(u, prom);
const stat = this.deviceTrackingStatus[u];
if (stat == TrackingStatus.PendingDownload) {
this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
@@ -643,11 +640,11 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this.keyDownloadsInProgressByUser[u] !== prom) {
if (this.keyDownloadsInProgressByUser.get(u) !== prom) {
logger.log('Another update in the queue for', u, '- not marking up-to-date');
return;
}
delete this.keyDownloadsInProgressByUser[u];
this.keyDownloadsInProgressByUser.delete(u);
const stat = this.deviceTrackingStatus[u];
if (stat == TrackingStatus.DownloadInProgress) {
if (success) {
@@ -687,9 +684,9 @@ class DeviceListUpdateSerialiser {
// deferred which is resolved when the queued users are downloaded.
// non-null indicates that we have users queued for download.
private queuedQueryDeferred: IDeferred<void> = null;
private queuedQueryDeferred?: IDeferred<void>;
private syncToken: string = null; // The sync token we send with the requests
private syncToken?: string; // The sync token we send with the requests
/*
* @param {object} baseApis Base API object
@@ -748,7 +745,7 @@ class DeviceListUpdateSerialiser {
const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
this.keyDownloadsQueuedByUser = {};
const deferred = this.queuedQueryDeferred;
this.queuedQueryDeferred = null;
this.queuedQueryDeferred = undefined;
logger.log('Starting key download for', downloadUsers);
this.downloadInProgress = true;
@@ -785,9 +782,9 @@ class DeviceListUpdateSerialiser {
try {
await this.processQueryResponseForUser(
userId, dk[userId], {
master: masterKeys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
master: masterKeys?.[userId],
self_signing: ssks?.[userId],
user_signing: usks?.[userId],
},
);
} catch (e) {
@@ -800,7 +797,7 @@ class DeviceListUpdateSerialiser {
logger.log('Completed key download for ' + downloadUsers);
this.downloadInProgress = false;
deferred.resolve();
deferred?.resolve();
// if we have queued users, fire off another request.
if (this.queuedQueryDeferred) {
@@ -809,19 +806,19 @@ class DeviceListUpdateSerialiser {
}, (e) => {
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this.downloadInProgress = false;
deferred.reject(e);
deferred?.reject(e);
});
return deferred.promise;
return deferred!.promise;
}
private async processQueryResponseForUser(
userId: string,
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
dkResponse: DeviceKeys,
crossSigningResponse: {
master: IDownloadKeyResult["master_keys"]["user_id"];
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
master?: Keys;
self_signing?: SigningKeys;
user_signing?: SigningKeys;
},
): Promise<void> {
logger.log('got device keys for ' + userId + ':', dkResponse);
@@ -840,7 +837,7 @@ class DeviceListUpdateSerialiser {
await updateStoredDeviceKeysForUser(
this.olmDevice, userId, userStore, dkResponse || {},
this.baseApis.getUserId(), this.baseApis.deviceId,
this.baseApis.getUserId()!, this.baseApis.deviceId!,
);
// put the updates into the object that will be returned as our results
+17 -17
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { IContent, MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, ClientPrefix } from "../http-api";
@@ -53,10 +53,10 @@ export class EncryptionSetupBuilder {
public readonly crossSigningCallbacks: CrossSigningCallbacks;
public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
private crossSigningKeys: ICrossSigningKeys = null;
private keySignatures: KeySignatures = null;
private keyBackupInfo: IKeyBackupInfo = null;
private sessionBackupPrivateKey: Uint8Array;
private crossSigningKeys?: ICrossSigningKeys;
private keySignatures?: KeySignatures;
private keyBackupInfo?: IKeyBackupInfo;
private sessionBackupPrivateKey?: Uint8Array;
/**
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
@@ -162,14 +162,14 @@ export class EncryptionSetupBuilder {
for (const type of ["master", "self_signing", "user_signing"]) {
logger.log(`Cache ${type} cross-signing private key locally`);
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
await cacheCallbacks.storeCrossSigningKeyCache?.(type, privateKey);
}
// store own cross-sign pubkeys as trusted
await crypto.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto.cryptoStore.storeCrossSigningKeys(
txn, this.crossSigningKeys.keys);
txn, this.crossSigningKeys!.keys);
},
);
}
@@ -195,9 +195,9 @@ export class EncryptionSetupOperation {
*/
constructor(
private readonly accountData: Map<string, object>,
private readonly crossSigningKeys: ICrossSigningKeys,
private readonly keyBackupInfo: IKeyBackupInfo,
private readonly keySignatures: KeySignatures,
private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo,
private readonly keySignatures?: KeySignatures,
) {}
/**
@@ -215,7 +215,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 => {
await this.crossSigningKeys.authUpload?.(authDict => {
return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
});
@@ -281,15 +281,15 @@ class AccountDataClientAdapter
* @param {String} type
* @return {Promise<Object>} the content of the account data
*/
public getAccountDataFromServer(type: string): Promise<any> {
return Promise.resolve(this.getAccountData(type));
public getAccountDataFromServer<T extends {[k: string]: any}>(type: string): Promise<T> {
return Promise.resolve(this.getAccountData(type) as T);
}
/**
* @param {String} type
* @return {Object} the content of the account data
*/
public getAccountData(type: string): MatrixEvent {
public getAccountData(type: string): IContent | null {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue;
@@ -329,7 +329,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
public readonly privateKeys = new Map<string, Uint8Array>();
// cache callbacks
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array> {
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array | null> {
return this.getCrossSigningKey(type, expectedPublicKey);
}
@@ -339,8 +339,8 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
}
// non-cache callbacks
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array> {
return Promise.resolve(this.privateKeys.get(type));
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array | null> {
return Promise.resolve(this.privateKeys.get(type) ?? null);
}
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
+135 -128
View File
@@ -15,9 +15,8 @@ limitations under the License.
*/
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
import { Logger } from "loglevel";
import { logger } from '../logger';
import { logger, PrefixedLogger } from '../logger';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import * as algorithms from './algorithms';
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
@@ -121,9 +120,9 @@ interface IInboundGroupSessionKey {
chain_index: number;
key: string;
forwarding_curve25519_key_chain: string[];
sender_claimed_ed25519_key: string;
sender_claimed_ed25519_key: string | null;
shared_history: boolean;
untrusted: boolean;
untrusted?: boolean;
}
/* eslint-enable camelcase */
@@ -145,9 +144,9 @@ export class OlmDevice {
public pickleKey = "DEFAULT_KEY"; // set by consumers
// don't know these until we load the account from storage in init()
public deviceCurve25519Key: string = null;
public deviceEd25519Key: string = null;
private maxOneTimeKeys: number = null;
public deviceCurve25519Key: string | null = null;
public deviceEd25519Key: string | null = null;
private maxOneTimeKeys: number | null = null;
// we don't bother stashing outboundgroupsessions in the cryptoStore -
// instead we keep them here.
@@ -266,8 +265,8 @@ export class OlmDevice {
lastReceivedMessageTs: session.lastReceivedMessageTs,
};
this.cryptoStore.storeEndToEndSession(
deviceKey,
sessionId,
deviceKey!,
sessionId!,
sessionInfo,
txn,
);
@@ -358,7 +357,7 @@ export class OlmDevice {
// is not exactly the same thing you get in method _getSession
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
result.sessions.push(pickledSession);
result.sessions!.push(pickledSession!);
});
},
);
@@ -384,8 +383,8 @@ export class OlmDevice {
func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
): void {
this.cryptoStore.getEndToEndSession(
deviceKey, sessionId, txn, (sessionInfo: ISessionInfo) => {
this.unpickleSession(sessionInfo, func);
deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => {
this.unpickleSession(sessionInfo!, func);
},
);
}
@@ -405,7 +404,7 @@ export class OlmDevice {
): void {
const session = new global.Olm.Session();
try {
session.unpickle(this.pickleKey, sessionInfo.session);
session.unpickle(this.pickleKey, sessionInfo.session!);
const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
func(unpickledSessInfo);
@@ -491,7 +490,7 @@ export class OlmDevice {
* @return {number} number of keys
*/
public maxNumberOfOneTimeKeys(): number {
return this.maxOneTimeKeys;
return this.maxOneTimeKeys ?? -1;
}
/**
@@ -554,7 +553,7 @@ export class OlmDevice {
});
},
);
return result;
return result!;
}
public async forgetOldFallbackKey(): Promise<void> {
@@ -607,7 +606,7 @@ export class OlmDevice {
},
logger.withPrefix("[createOutboundSession]"),
);
return newSessionId;
return newSessionId!;
}
/**
@@ -668,7 +667,7 @@ export class OlmDevice {
logger.withPrefix("[createInboundSession]"),
);
return result;
return result!;
}
/**
@@ -681,7 +680,7 @@ export class OlmDevice {
public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
const log = logger.withPrefix("[getSessionIdsForDevice]");
if (this.sessionsInProgress[theirDeviceIdentityKey]) {
if (theirDeviceIdentityKey in this.sessionsInProgress) {
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
try {
await this.sessionsInProgress[theirDeviceIdentityKey];
@@ -703,7 +702,7 @@ export class OlmDevice {
log,
);
return sessionIds;
return sessionIds!;
}
/**
@@ -714,13 +713,13 @@ export class OlmDevice {
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log
* @param {PrefixedLogger} [log] A possibly customised log
* @return {Promise<?string>} session id, or null if no established session
*/
public async getSessionIdForDevice(
theirDeviceIdentityKey: string,
nowait = false,
log?: Logger,
log?: PrefixedLogger,
): Promise<string | null> {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
@@ -771,7 +770,7 @@ export class OlmDevice {
): Promise<{ sessionId: string, lastReceivedMessageTs: number, hasReceivedMessage: boolean }[]> {
log = log.withPrefix("[getSessionInfoForDevice]");
if (this.sessionsInProgress[deviceIdentityKey] && !nowait) {
if (deviceIdentityKey in this.sessionsInProgress && !nowait) {
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
try {
await this.sessionsInProgress[deviceIdentityKey];
@@ -780,7 +779,11 @@ export class OlmDevice {
// return an empty result
}
}
const info = [];
const info: {
lastReceivedMessageTs: number;
hasReceivedMessage: boolean;
sessionId: string;
}[] = [];
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
@@ -790,9 +793,9 @@ export class OlmDevice {
for (const sessionId of sessionIds) {
this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => {
info.push({
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs,
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!,
hasReceivedMessage: sessInfo.session.has_received_message(),
sessionId: sessionId,
sessionId,
});
});
}
@@ -801,7 +804,7 @@ export class OlmDevice {
log,
);
return info;
return info!;
}
/**
@@ -916,7 +919,7 @@ export class OlmDevice {
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
}
@@ -1056,10 +1059,14 @@ export class OlmDevice {
senderKey: string,
sessionId: string,
txn: unknown,
func: (session: InboundGroupSession, data: InboundGroupSessionData, withheld?: IWithheld) => void,
func: (
session: InboundGroupSession | null,
data: InboundGroupSessionData | null,
withheld: IWithheld | null,
) => void,
): void {
this.cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData: InboundGroupSessionData, withheld: IWithheld | null) => {
senderKey, sessionId, txn, (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => {
if (sessionData === null) {
func(null, null, withheld);
return;
@@ -1112,94 +1119,94 @@ export class OlmDevice {
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
/* if we already have this session, consider updating it */
this.getInboundGroupSession(
roomId, senderKey, sessionId, txn,
(existingSession: InboundGroupSession, existingSessionData: InboundGroupSessionData) => {
// new session.
const session = new global.Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index() <= session.first_known_index()) {
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
// existing session has less-than-or-equal index
// (i.e. can decrypt at least as much), and the
// new session's trust does not win over the old
// session's trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`);
return;
}
if (existingSession.first_known_index() < session.first_known_index()) {
// We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
}
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
);
const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this.pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
});
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this.cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (
existingSession: InboundGroupSession | null,
existingSessionData: InboundGroupSessionData | null,
) => {
// new session.
const session = new global.Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
},
);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index() <= session.first_known_index()) {
if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
// existing session has less-than-or-equal index
// (i.e. can decrypt at least as much), and the
// new session's trust does not win over the old
// session's trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`);
return;
}
if (existingSession.first_known_index() < session.first_known_index()) {
// We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData!.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData!, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
}
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
);
const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this.pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
});
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this.cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
}
});
},
logger.withPrefix("[addInboundGroupSession]"),
);
@@ -1261,7 +1268,7 @@ export class OlmDevice {
eventId: string,
timestamp: number,
): Promise<IDecryptedGroupMessage | null> {
let result: IDecryptedGroupMessage;
let result: IDecryptedGroupMessage | null = null;
// when the localstorage crypto store is used as an indexeddb backend,
// exceptions thrown from within the inner function are not passed through
// to the top level, so we store exceptions in a variable and raise them at
@@ -1275,7 +1282,7 @@ export class OlmDevice {
], (txn) => {
this.getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) {
if (session === null || sessionData === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
@@ -1292,7 +1299,7 @@ export class OlmDevice {
try {
res = session.decrypt(body);
} catch (e) {
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
if ((<Error>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
calculateWithheldMessage(withheld),
@@ -1301,7 +1308,7 @@ export class OlmDevice {
},
);
} else {
error = e;
error = <Error>e;
}
return;
}
@@ -1350,7 +1357,7 @@ export class OlmDevice {
forwardingCurve25519KeyChain: (
sessionData.forwardingCurve25519KeyChain || []
),
untrusted: sessionData.untrusted,
untrusted: !!sessionData.untrusted,
};
},
);
@@ -1358,10 +1365,10 @@ export class OlmDevice {
logger.withPrefix("[decryptGroupMessage]"),
);
if (error) {
if (error!) {
throw error;
}
return result;
return result!;
}
/**
@@ -1404,7 +1411,7 @@ export class OlmDevice {
logger.withPrefix("[hasInboundSessionKeys]"),
);
return result;
return result!;
}
/**
@@ -1431,8 +1438,8 @@ export class OlmDevice {
senderKey: string,
sessionId: string,
chainIndex?: number,
): Promise<IInboundGroupSessionKey> {
let result: IInboundGroupSessionKey;
): Promise<IInboundGroupSessionKey | null> {
let result: IInboundGroupSessionKey | null = null;
await this.cryptoStore.doTxn(
'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
@@ -1440,7 +1447,7 @@ export class OlmDevice {
], (txn) => {
this.getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) {
if (session === null || sessionData === null) {
result = null;
return;
}
@@ -1520,7 +1527,7 @@ export class OlmDevice {
},
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
);
return result;
return result!;
}
// Utilities
+48 -33
View File
@@ -75,10 +75,26 @@ export enum RoomKeyRequestState {
CancellationPendingAndWillResend,
}
interface RequestMessageBase {
requesting_device_id: string;
request_id: string;
}
interface RequestMessageRequest extends RequestMessageBase {
action: "request";
body: IRoomKeyRequestBody;
}
interface RequestMessageCancellation extends RequestMessageBase {
action: "request_cancellation";
}
type RequestMessage = RequestMessageRequest | RequestMessageCancellation;
export class OutgoingRoomKeyRequestManager {
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
private sendOutgoingRoomKeyRequestsTimer: ReturnType<typeof setTimeout> = null;
private sendOutgoingRoomKeyRequestsTimer?: ReturnType<typeof setTimeout>;
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequests
@@ -369,43 +385,42 @@ export class OutgoingRoomKeyRequestManager {
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
private sendOutgoingRoomKeyRequests(): Promise<void> {
private async sendOutgoingRoomKeyRequests(): Promise<void> {
if (!this.clientRunning) {
this.sendOutgoingRoomKeyRequestsTimer = null;
return Promise.resolve();
this.sendOutgoingRoomKeyRequestsTimer = undefined;
return;
}
return this.cryptoStore.getOutgoingRoomKeyRequestByState([
const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([
RoomKeyRequestState.CancellationPending,
RoomKeyRequestState.CancellationPendingAndWillResend,
RoomKeyRequestState.Unsent,
]).then((req: OutgoingRoomKeyRequest) => {
if (!req) {
this.sendOutgoingRoomKeyRequestsTimer = null;
return;
}
]);
let prom;
if (!req) {
this.sendOutgoingRoomKeyRequestsTimer = undefined;
return;
}
try {
switch (req.state) {
case RoomKeyRequestState.Unsent:
prom = this.sendOutgoingRoomKeyRequest(req);
await this.sendOutgoingRoomKeyRequest(req);
break;
case RoomKeyRequestState.CancellationPending:
prom = this.sendOutgoingRoomKeyRequestCancellation(req);
await this.sendOutgoingRoomKeyRequestCancellation(req);
break;
case RoomKeyRequestState.CancellationPendingAndWillResend:
prom = this.sendOutgoingRoomKeyRequestCancellation(req, true);
await this.sendOutgoingRoomKeyRequestCancellation(req, true);
break;
}
return prom.then(() => {
// go around the loop again
return this.sendOutgoingRoomKeyRequests();
}).catch((e) => {
logger.error("Error sending room key request; will retry later.", e);
this.sendOutgoingRoomKeyRequestsTimer = null;
});
});
// go around the loop again
return this.sendOutgoingRoomKeyRequests();
} catch (e) {
logger.error("Error sending room key request; will retry later.", e);
this.sendOutgoingRoomKeyRequestsTimer = undefined;
}
}
// given a RoomKeyRequest, send it and update the request record
@@ -416,16 +431,14 @@ export class OutgoingRoomKeyRequestManager {
`(id ${req.requestId})`,
);
const requestMessage = {
const requestMessage: RequestMessage = {
action: "request",
requesting_device_id: this.deviceId,
request_id: req.requestId,
body: req.requestBody,
};
return this.sendMessageToDevices(
requestMessage, req.recipients, req.requestTxnId || req.requestId,
).then(() => {
return this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => {
return this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent },
@@ -443,7 +456,7 @@ export class OutgoingRoomKeyRequestManager {
`(cancellation id ${req.cancellationTxnId})`,
);
const requestMessage = {
const requestMessage: RequestMessage = {
action: "request_cancellation",
requesting_device_id: this.deviceId,
request_id: req.requestId,
@@ -467,7 +480,11 @@ export class OutgoingRoomKeyRequestManager {
}
// send a RoomKeyRequest to a list of recipients
private sendMessageToDevices(message, recipients, txnId: string): Promise<{}> {
private sendMessageToDevices(
message: RequestMessage,
recipients: IRoomKeyRequestRecipient[],
txnId?: string,
): Promise<{}> {
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
@@ -480,15 +497,13 @@ export class OutgoingRoomKeyRequestManager {
}
}
function stringifyRequestBody(requestBody) {
function stringifyRequestBody(requestBody: IRoomKeyRequestBody): string {
// we assume that the request is for megolm keys, which are identified by
// room id and session id
return requestBody.room_id + " / " + requestBody.session_id;
}
function stringifyRecipientList(recipients) {
return '['
+ recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")
+ ']';
function stringifyRecipientList(recipients: IRoomKeyRequestRecipient[]): string {
return `[${recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")}]`;
}
+5 -5
View File
@@ -38,12 +38,12 @@ export class RoomList {
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
private roomEncryption: Record<string, IRoomEncryption> = {};
constructor(private readonly cryptoStore: CryptoStore) {}
constructor(private readonly cryptoStore?: CryptoStore) {}
public async init(): Promise<void> {
await this.cryptoStore.doTxn(
await this.cryptoStore!.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this.cryptoStore.getEndToEndRooms(txn, (result) => {
this.cryptoStore!.getEndToEndRooms(txn, (result) => {
this.roomEncryption = result;
});
},
@@ -63,9 +63,9 @@ export class RoomList {
// as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events
this.roomEncryption[roomId] = roomInfo;
await this.cryptoStore.doTxn(
await this.cryptoStore!.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
this.cryptoStore!.storeEndToEndRoom(roomId, roomInfo, txn);
},
);
}
+33 -44
View File
@@ -19,10 +19,11 @@ import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring';
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
import { ClientEvent, IContent, ICryptoCallbacks, MatrixEvent } from '../matrix';
import { ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { TypedEventEmitter } from '../models/typed-event-emitter';
import { defer, IDeferred } from "../utils";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -39,15 +40,14 @@ export interface ISecretRequest {
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
getAccountData: (eventType: string) => MatrixEvent;
getAccountData: (eventType: string) => IContent | null;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
interface ISecretRequestInternal {
name: string;
devices: string[];
resolve: (reason: string) => void;
reject: (error: Error) => void;
deferred: IDeferred<string>;
}
interface IDecryptors {
@@ -66,7 +66,7 @@ interface ISecretInfo {
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export class SecretStorage {
export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
private requests = new Map<string, ISecretRequestInternal>();
// In it's pure javascript days, this was relying on some proper Javascript-style
@@ -80,7 +80,7 @@ export class SecretStorage {
constructor(
private readonly accountDataAdapter: IAccountDataClient,
private readonly cryptoCallbacks: ICryptoCallbacks,
private readonly baseApis?: MatrixClient,
private readonly baseApis: B,
) {}
public async getDefaultKeyId(): Promise<string | null> {
@@ -129,13 +129,11 @@ export class SecretStorage {
*/
public async addKey(
algorithm: string,
opts: IAddSecretStorageKeyOpts,
opts: IAddSecretStorageKeyOpts = {},
keyId?: string,
): Promise<SecretStorageKeyObject> {
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
if (opts.name) {
keyInfo.name = opts.name;
}
@@ -182,7 +180,7 @@ export class SecretStorage {
* the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
public async getKey(keyId?: string | null): Promise<SecretStorageKeyTuple | null> {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
@@ -237,7 +235,7 @@ export class SecretStorage {
* @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
const encrypted: Record<string, IEncryptedPayload> = {};
if (!keys) {
@@ -284,7 +282,7 @@ export class SecretStorage {
*
* @return {string} the contents of the secret
*/
public async get(name: string): Promise<string> {
public async get(name: string): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) {
return;
@@ -376,21 +374,11 @@ export class SecretStorage {
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
*/
public request(name: string, devices: string[]): ISecretRequest {
public request(this: SecretStorage<MatrixClient>, name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
let resolve: (s: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
this.requests.set(requestId, {
name,
devices,
resolve,
reject,
});
const deferred = defer<string>();
this.requests.set(requestId, { name, devices, deferred });
const cancel = (reason: string) => {
// send cancellation event
@@ -404,12 +392,12 @@ export class SecretStorage {
toDevice[device] = cancelData;
}
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
[this.baseApis.getUserId()!]: toDevice,
});
// and reject the promise so that anyone waiting on it will be
// notified
reject(new Error(reason || "Cancelled"));
deferred.reject(new Error(reason || "Cancelled"));
};
// send request to devices
@@ -425,22 +413,23 @@ export class SecretStorage {
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
[this.baseApis.getUserId()!]: toDevice,
});
return {
requestId,
promise,
promise: deferred.promise,
cancel,
};
}
public async onRequestReceived(event: MatrixEvent): Promise<void> {
public async onRequestReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): Promise<void> {
const sender = event.getSender();
const content = event.getContent();
if (sender !== this.baseApis.getUserId()
|| !(content.name && content.action
&& content.requesting_device_id && content.request_id)) {
&& content.requesting_device_id && content.request_id)
) {
// ignore requests from anyone else, for now
return;
}
@@ -498,25 +487,25 @@ export class SecretStorage {
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.ensureOlmSessionsForDevices(
this.baseApis.crypto.olmDevice,
this.baseApis.crypto!.olmDevice,
this.baseApis,
{
[sender]: [
this.baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId)!,
],
},
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.baseApis.getUserId(),
this.baseApis.deviceId,
this.baseApis.crypto.olmDevice,
this.baseApis.getUserId()!,
this.baseApis.deviceId!,
this.baseApis.crypto!.olmDevice,
sender,
this.baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId)!,
payload,
);
const contentMap = {
@@ -533,7 +522,7 @@ export class SecretStorage {
}
}
public onSecretReceived(event: MatrixEvent): void {
public onSecretReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): void {
if (event.getSender() !== this.baseApis.getUserId()) {
// we shouldn't be receiving secrets from anyone else, so ignore
// because someone could be trying to send us bogus data
@@ -547,7 +536,7 @@ export class SecretStorage {
const content = event.getContent();
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey() || "",
);
@@ -561,9 +550,9 @@ export class SecretStorage {
if (requestControl) {
// make sure that the device that sent it is one of the devices that
// we requested from
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
const deviceInfo = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey(),
event.getSenderKey()!,
);
if (!deviceInfo) {
logger.log(
@@ -578,7 +567,7 @@ export class SecretStorage {
// unsure that the sender is trusted. In theory, this check is
// unnecessary since we only accept secret shares from devices that
// we requested from, but it doesn't hurt.
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
const deviceTrust = this.baseApis.crypto!.checkDeviceInfoTrust(event.getSender()!, deviceInfo);
if (!deviceTrust.isVerified()) {
logger.log("secret share from unverified device");
return;
@@ -588,7 +577,7 @@ export class SecretStorage {
`Successfully received secret ${requestControl.name} ` +
`from ${deviceInfo.deviceId}`,
);
requestControl.resolve(content.secret);
requestControl.deferred.resolve(content.secret);
}
}
+16 -12
View File
@@ -36,7 +36,7 @@ import { IRoomEncryption } from "../RoomList";
*/
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
export type DecryptionClassParams<P extends IParams = IParams> = Omit<P, "deviceId" | "config">;
/**
* map of registered encryption algorithm classes. Map from string to {@link
@@ -52,7 +52,7 @@ export interface IParams {
crypto: Crypto;
olmDevice: OlmDevice;
baseApis: MatrixClient;
roomId: string;
roomId?: string;
config: IRoomEncryption & object;
}
@@ -76,7 +76,7 @@ export abstract class EncryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId: string;
protected readonly roomId?: string;
constructor(params: IParams) {
this.userId = params.userId;
@@ -148,7 +148,7 @@ export abstract class DecryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId: string;
protected readonly roomId?: string;
constructor(params: DecryptionClassParams) {
this.userId = params.userId;
@@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm {
export class DecryptionError extends Error {
public readonly detailedString: string;
constructor(public readonly code: string, msg: string, details?: Record<string, string>) {
constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
@@ -250,7 +250,7 @@ export class DecryptionError extends Error {
}
}
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string {
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
let result = err.name + '[msg: ' + err.message;
if (details) {
@@ -272,7 +272,11 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
* @extends Error
*/
export class UnknownDeviceError extends Error {
constructor(msg: string, public readonly devices: Record<string, Record<string, object>>) {
constructor(
msg: string,
public readonly devices: Record<string, Record<string, object>>,
public event?: MatrixEvent,
) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
@@ -292,11 +296,11 @@ export class UnknownDeviceError extends Error {
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
export function registerAlgorithm(
export function registerAlgorithm<P extends IParams = IParams>(
algorithm: string,
encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
encryptor: new (params: P) => EncryptionAlgorithm,
decryptor: new (params: DecryptionClassParams<P>) => DecryptionAlgorithm,
): void {
ENCRYPTION_CLASSES.set(algorithm, encryptor);
DECRYPTION_CLASSES.set(algorithm, decryptor);
ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm);
DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm);
}
+82 -35
View File
@@ -24,6 +24,7 @@ import { logger } from '../../logger';
import * as olmlib from "../olmlib";
import {
DecryptionAlgorithm,
DecryptionClassParams,
DecryptionError,
EncryptionAlgorithm,
IParams,
@@ -36,9 +37,11 @@ import { DeviceInfo } from "../deviceinfo";
import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../../models/event";
import { EventType, MsgType } from '../../@types/event';
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api";
// determine whether the key can be shared with invitees
export function isRoomSharedHistory(room: Room): boolean {
@@ -249,8 +252,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
startTime: number;
};
constructor(params: IParams) {
protected readonly roomId: string;
constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
super(params);
this.roomId = params.roomId;
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000;
@@ -491,13 +497,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
const key = this.olmDevice.getOutboundGroupSessionKey(sessionId);
await this.olmDevice.addInboundGroupSession(
this.roomId, this.olmDevice.deviceCurve25519Key, [], sessionId,
key.key, { ed25519: this.olmDevice.deviceEd25519Key }, false,
this.roomId, this.olmDevice.deviceCurve25519Key!, [], sessionId,
key.key, { ed25519: this.olmDevice.deviceEd25519Key! }, false,
{ sharedHistory },
);
// don't wait for it to complete
this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key, sessionId);
this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory);
}
@@ -928,7 +934,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
room_id: this.roomId,
session_id: session.sessionId,
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
};
const userDeviceMaps = this.splitDevices(devicesByUser);
@@ -1019,7 +1025,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
}
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
/**
* When using in-room messages and the room has encryption enabled,
* clients should ensure that encryption does not hinder the verification.
*/
const forceDistributeToUnverified = this.isVerificationEvent(eventType, content);
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room, forceDistributeToUnverified);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
@@ -1053,6 +1064,26 @@ class MegolmEncryption extends EncryptionAlgorithm {
return encryptedContent;
}
private isVerificationEvent(eventType: string, content: object): boolean {
switch (eventType) {
case EventType.KeyVerificationCancel:
case EventType.KeyVerificationDone:
case EventType.KeyVerificationMac:
case EventType.KeyVerificationStart:
case EventType.KeyVerificationKey:
case EventType.KeyVerificationReady:
case EventType.KeyVerificationAccept: {
return true;
}
case EventType.RoomMessage: {
return content['msgtype'] === MsgType.KeyVerificationRequest;
}
default: {
return false;
}
}
}
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
@@ -1119,6 +1150,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Get the list of unblocked devices for all users in the room
*
* @param {module:models/room} room
* @param forceDistributeToUnverified if set to true will include the unverified devices
* even if setting is set to block them (useful for verification)
*
* @return {Promise} Promise which resolves to an array whose
* first element is a map from userId to deviceId to deviceInfo indicating
@@ -1126,7 +1159,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
* element is a map from userId to deviceId to data indicating the devices
* that are in the room but that have been blocked
*/
private async getDevicesInRoom(room: Room): Promise<[DeviceInfoMap, IBlockedMap]> {
private async getDevicesInRoom(
room: Room,
forceDistributeToUnverified = false,
): Promise<[DeviceInfoMap, IBlockedMap]> {
const members = await room.getEncryptionTargetMembers();
const roomMembers = members.map(function(u) {
return u.userId;
@@ -1134,8 +1170,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
// The global value is treated as a default for when rooms don't specify a value.
let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices();
if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') {
isBlacklisting = room.getBlacklistUnverifiedDevices();
const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
if (typeof isRoomBlacklisting === 'boolean') {
isBlacklisting = isRoomBlacklisting;
}
// We are happy to use a cached version here: we assume that if we already
@@ -1161,7 +1198,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
if (userDevices[deviceId].isBlocked() ||
(!deviceTrust.isVerified() && isBlacklisting)
(!deviceTrust.isVerified() && isBlacklisting && !forceDistributeToUnverified)
) {
if (!blocked[userId]) {
blocked[userId] = {};
@@ -1199,6 +1236,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
// this gets stubbed out by the unit tests.
private olmlib = olmlib;
protected readonly roomId: string;
constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
super(params);
this.roomId = params.roomId;
}
/**
* @inheritdoc
*
@@ -1228,21 +1272,21 @@ class MegolmDecryption extends DecryptionAlgorithm {
// (fixes https://github.com/vector-im/element-web/issues/5001)
this.addEventToPendingList(event);
let res: IDecryptedGroupMessage;
let res: IDecryptedGroupMessage | null;
try {
res = await this.olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
event.getId(), event.getTs(),
event.getRoomId()!, content.sender_key, content.session_id, content.ciphertext,
event.getId()!, event.getTs(),
);
} catch (e) {
if (e.name === "DecryptionError") {
if ((<Error>e).name === "DecryptionError") {
// re-throw decryption errors as-is
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
if ((<MatrixError>e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this.requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
@@ -1332,7 +1376,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
const recipients = event.getKeyRequestRecipients(this.userId);
this.crypto.requestRoomKey({
room_id: event.getRoomId(),
room_id: event.getRoomId()!,
algorithm: wireContent.algorithm,
sender_key: wireContent.sender_key,
session_id: wireContent.session_id,
@@ -1353,7 +1397,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
if (!this.pendingEvents.has(senderKey)) {
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
}
const senderPendingEvents = this.pendingEvents.get(senderKey);
const senderPendingEvents = this.pendingEvents.get(senderKey)!;
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
@@ -1379,9 +1423,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
pendingEvents.delete(event);
if (pendingEvents.size === 0) {
senderPendingEvents.delete(sessionId);
senderPendingEvents!.delete(sessionId);
}
if (senderPendingEvents.size === 0) {
if (senderPendingEvents!.size === 0) {
this.pendingEvents.delete(senderKey);
}
}
@@ -1393,7 +1437,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
*/
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent<Partial<IMessage["content"]>>();
let senderKey = event.getSenderKey();
let senderKey = event.getSenderKey()!;
let forwardingKeyChain: string[] = [];
let exportFormat = false;
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
@@ -1423,7 +1467,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
olmlib.OLM_ALGORITHM,
senderKey,
);
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
@@ -1432,7 +1476,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
return;
}
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
event.getSender()!, deviceInfo.deviceId, [RoomKeyRequestState.Sent],
) : [];
const weRequested = outgoingRequests.some((req) => (
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
@@ -1492,7 +1536,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
// that room later
if (!room) {
const parkedData = {
senderId: event.getSender(),
senderId: event.getSender()!,
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: content.session_key,
@@ -1502,14 +1546,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
await this.crypto.cryptoStore.doTxn(
'readwrite',
['parked_shared_history'],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id!, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"),
);
return;
}
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
) ?? undefined;
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
if (fromUs && !deviceTrust.isVerified()) {
return;
@@ -1573,7 +1620,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
const senderKey = content.sender_key;
if (content.code === "m.no_olm") {
const sender = event.getSender();
const sender = event.getSender()!;
logger.warn(
`${sender}:${senderKey} was unable to establish an olm session with us`,
);
@@ -1667,7 +1714,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId);
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
const body = keyRequest.requestBody;
this.olmlib.ensureOlmSessionsForDevices(
@@ -1708,7 +1755,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
this.olmDevice,
userId,
deviceInfo,
payload,
payload!,
).then(() => {
const contentMap = {
[userId]: {
@@ -1735,12 +1782,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"sender_key": senderKey,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
"sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!,
"session_id": sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
"session_key": key!.key,
"chain_index": key!.chain_index,
"forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key!.shared_history || false,
},
};
}
@@ -1870,7 +1917,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
for (const deviceInfo of devices) {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
};
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
+11 -11
View File
@@ -180,14 +180,14 @@ class OlmDecryption extends DecryptionAlgorithm {
);
}
if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) {
if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) {
throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this.olmDevice.deviceCurve25519Key];
let payloadString;
const message = ciphertext[this.olmDevice.deviceCurve25519Key!];
let payloadString: string;
try {
payloadString = await this.decryptMessage(deviceKey, message);
@@ -196,7 +196,7 @@ class OlmDecryption extends DecryptionAlgorithm {
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
err: e as Error,
},
);
}
@@ -217,7 +217,7 @@ class OlmDecryption extends DecryptionAlgorithm {
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this.olmDevice.deviceEd25519Key,
our_key: this.olmDevice.deviceEd25519Key!,
},
);
}
@@ -228,12 +228,12 @@ class OlmDecryption extends DecryptionAlgorithm {
// assume that the device logged out. Some event handlers, such as
// secret sharing, may be more strict and reject events that come from
// unknown devices.
await this.crypto.deviceList.downloadKeys([event.getSender()], false);
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
deviceKey,
);
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
throw new DecryptionError(
"OLM_BAD_SENDER",
"Message claimed to be from " + event.getSender(), {
@@ -250,7 +250,7 @@ class OlmDecryption extends DecryptionAlgorithm {
throw new DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
reported_sender: event.getSender()!,
},
);
}
@@ -325,13 +325,13 @@ class OlmDecryption extends DecryptionAlgorithm {
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message,
sessionId + ": " + (<Error>e).message,
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
decryptionErrors[sessionId] = (<Error>e).message;
}
}
@@ -358,7 +358,7 @@ class OlmDecryption extends DecryptionAlgorithm {
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
decryptionErrors["(new)"] = (<Error>e).message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors),
+2 -2
View File
@@ -117,10 +117,10 @@ export interface IPassphraseInfo {
}
export interface IAddSecretStorageKeyOpts {
pubkey: string;
pubkey?: string;
passphrase?: IPassphraseInfo;
name?: string;
key: Uint8Array;
key?: Uint8Array;
}
export interface IImportOpts {
+64 -74
View File
@@ -40,6 +40,7 @@ import {
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent, IMegolmSessionData } from "./index";
import { crypto } from "./crypto";
import { HTTPError, MatrixError } from "../http-api";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -62,7 +63,7 @@ export type TrustInfo = {
};
export interface IKeyBackupCheck {
backupInfo: IKeyBackupInfo;
backupInfo?: IKeyBackupInfo;
trustInfo: TrustInfo;
}
@@ -85,9 +86,7 @@ interface BackupAlgorithmClass {
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
// prepare a brand new backup
prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]>;
prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]>;
checkBackupVersion(info: IKeyBackupInfo): void;
}
@@ -202,7 +201,7 @@ export class BackupManager {
}
const [privateKey, authData] = await Algorithm.prepare(key);
const recoveryKey = encodeRecoveryKey(privateKey);
const recoveryKey = encodeRecoveryKey(privateKey)!;
return {
algorithm: Algorithm.algorithmName,
auth_data: authData,
@@ -221,19 +220,19 @@ export class BackupManager {
* one of the user's verified devices, start backing up
* to it.
*/
public async checkAndStart(): Promise<IKeyBackupCheck> {
public async checkAndStart(): Promise<IKeyBackupCheck | null> {
logger.log("Checking key backup status...");
if (this.baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest");
this.checkedForBackup = true;
return null;
}
let backupInfo: IKeyBackupInfo;
let backupInfo: IKeyBackupInfo | undefined;
try {
backupInfo = await this.baseApis.getKeyBackupVersion();
backupInfo = await this.baseApis.getKeyBackupVersion() ?? undefined;
} catch (e) {
logger.log("Error checking for active key backup", e);
if (e.httpStatus === 404) {
if ((<HTTPError>e).httpStatus === 404) {
// 404 is returned when the key backup does not exist, so that
// counts as successfully checking.
this.checkedForBackup = true;
@@ -245,11 +244,8 @@ export class BackupManager {
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
await this.enableKeyBackup(backupInfo);
logger.log(`Found usable key backup v${backupInfo!.version}: enabling key backups`);
await this.enableKeyBackup(backupInfo!);
} else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup");
this.disableKeyBackup();
@@ -257,13 +253,11 @@ export class BackupManager {
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
if (backupInfo!.version !== this.backupInfo.version) {
logger.log(`On backup version ${this.backupInfo.version} but ` +
`found version ${backupInfo!.version}: switching.`);
this.disableKeyBackup();
await this.enableKeyBackup(backupInfo);
await this.enableKeyBackup(backupInfo!);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
@@ -271,7 +265,7 @@ export class BackupManager {
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log("Backup version " + backupInfo.version + " still current");
logger.log(`Backup version ${backupInfo!.version} still current`);
}
}
@@ -287,7 +281,7 @@ export class BackupManager {
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*/
public async checkKeyBackup(): Promise<IKeyBackupCheck> {
public async checkKeyBackup(): Promise<IKeyBackupCheck | null> {
this.checkedForBackup = false;
return this.checkAndStart();
}
@@ -304,10 +298,10 @@ export class BackupManager {
const now = new Date().getTime();
if (
!this.sessionLastCheckAttemptedTime[targetSessionId]
|| now - this.sessionLastCheckAttemptedTime[targetSessionId] > KEY_BACKUP_CHECK_RATE_LIMIT
!this.sessionLastCheckAttemptedTime[targetSessionId!]
|| now - this.sessionLastCheckAttemptedTime[targetSessionId!] > KEY_BACKUP_CHECK_RATE_LIMIT
) {
this.sessionLastCheckAttemptedTime[targetSessionId] = now;
this.sessionLastCheckAttemptedTime[targetSessionId!] = now;
await this.baseApis.restoreKeyBackupWithCache(targetRoomId, targetSessionId, this.backupInfo, {});
}
}
@@ -325,7 +319,7 @@ export class BackupManager {
* ]
* }
*/
public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise<TrustInfo> {
public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise<TrustInfo> {
const ret = {
usable: false,
trusted_locally: false,
@@ -342,9 +336,10 @@ export class BackupManager {
return ret;
}
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey();
const userId = this.baseApis.getUserId()!;
const privKey = await this.baseApis.crypto!.getSessionBackupPrivateKey();
if (privKey) {
let algorithm;
let algorithm: BackupAlgorithm | null = null;
try {
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
@@ -356,13 +351,11 @@ export class BackupManager {
// do nothing -- if we have an error, then we don't mark it as
// locally trusted
} finally {
if (algorithm) {
algorithm.free();
}
algorithm?.free();
}
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
const mySigs = backupInfo.auth_data.signatures[userId] || {};
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
@@ -375,14 +368,14 @@ export class BackupManager {
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key
const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId();
const crossSigningId = this.baseApis.crypto!.crossSigningInfo.getId();
if (crossSigningId === sigInfo.deviceId) {
sigInfo.crossSigningId = true;
try {
await verifySignature(
this.baseApis.crypto.olmDevice,
this.baseApis.crypto!.olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
userId,
sigInfo.deviceId,
crossSigningId,
);
@@ -400,17 +393,16 @@ export class BackupManager {
// Now look for a sig from a device
// At some point this can probably go away and we'll just support
// it being signed by the cross-signing master key
const device = this.baseApis.crypto.deviceList.getStoredDevice(
this.baseApis.getUserId(), sigInfo.deviceId,
const device = this.baseApis.crypto!.deviceList.getStoredDevice(userId, sigInfo.deviceId,
);
if (device) {
sigInfo.device = device;
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId);
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId);
try {
await verifySignature(
this.baseApis.crypto.olmDevice,
this.baseApis.crypto!.olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
userId,
device.deviceId,
device.getFingerprint(),
);
@@ -431,12 +423,7 @@ export class BackupManager {
}
ret.usable = ret.sigs.some((s) => {
return (
s.valid && (
(s.device && s.deviceTrust.isVerified()) ||
(s.crossSigningId)
)
);
return s.valid && ((s.device && s.deviceTrust?.isVerified()) || (s.crossSigningId));
});
return ret;
}
@@ -474,17 +461,17 @@ export class BackupManager {
} catch (err) {
numFailures++;
logger.log("Key backup request failed", err);
if (err.data) {
if ((<MatrixError>err).data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
(<MatrixError>err).data.errcode == 'M_NOT_FOUND' ||
(<MatrixError>err).data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Re-check key backup status on error, so we can be
// sure to present the current situation when asked.
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (<MatrixError>err).data.errcode!);
throw err;
}
}
@@ -507,50 +494,50 @@ export class BackupManager {
* @returns {number} Number of sessions backed up
*/
public async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
let remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
const roomId = session.sessionData!.room_id;
if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} };
}
const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData!,
);
sessionData.algorithm = MEGOLM_ALGORITHM;
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey(
const userId = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
const device = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
) ?? undefined;
const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
rooms[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
session_data: await this.algorithm.encryptSession(sessionData),
session_data: await this.algorithm!.encryptSession(sessionData),
};
}
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
await this.baseApis.crypto!.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return sessions.length;
}
@@ -558,7 +545,7 @@ export class BackupManager {
public async backupGroupSession(
senderKey: string, sessionId: string,
): Promise<void> {
await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{
await this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
@@ -590,22 +577,22 @@ export class BackupManager {
* (which will be equal to the number of sessions in the store).
*/
public async flagAllGroupSessionsForBackup(): Promise<number> {
await this.baseApis.crypto.cryptoStore.doTxn(
await this.baseApis.crypto!.cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP,
],
(txn) => {
this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
this.baseApis.crypto!.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) {
this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn);
this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([session], txn);
}
});
},
);
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
const remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return remaining;
}
@@ -615,7 +602,7 @@ export class BackupManager {
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/
public countSessionsNeedingBackup(): Promise<number> {
return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
}
}
@@ -641,7 +628,7 @@ export class Curve25519 implements BackupAlgorithm {
}
public static async prepare(
key: string | Uint8Array | null,
key?: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
const decryption = new global.Olm.PkDecryption();
try {
@@ -741,7 +728,10 @@ function randomBytes(size: number): Uint8Array {
return buf;
}
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
const UNSTABLE_MSC3270_NAME = new UnstableValue(
"m.megolm_backup.v1.aes-hmac-sha2",
"org.matrix.msc3270.v1.aes-hmac-sha2",
);
export class Aes256 implements BackupAlgorithm {
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
@@ -769,7 +759,7 @@ export class Aes256 implements BackupAlgorithm {
}
public static async prepare(
key: string | Uint8Array | null,
key?: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
let outKey: Uint8Array;
const authData: Partial<IAes256AuthData> = {};
+14 -14
View File
@@ -58,9 +58,9 @@ const oneweek = 7 * 24 * 60 * 60 * 1000;
export class DehydrationManager {
private inProgress = false;
private timeoutId: any;
private key: Uint8Array;
private keyInfo: {[props: string]: any};
private deviceDisplayName: string;
private key?: Uint8Array;
private keyInfo?: {[props: string]: any};
private deviceDisplayName?: string;
constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache();
@@ -97,7 +97,7 @@ export class DehydrationManager {
/** set the key, and queue periodic dehydration to the server in the background */
public async setKeyAndQueueDehydration(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
deviceDisplayName?: string,
): Promise<void> {
const matches = await this.setKey(key, keyInfo, deviceDisplayName);
if (!matches) {
@@ -108,8 +108,8 @@ export class DehydrationManager {
public async setKey(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<boolean> {
deviceDisplayName?: string,
): Promise<boolean | undefined> {
if (!key) {
// unsetting the key -- cancel any pending dehydration task
if (this.timeoutId) {
@@ -135,9 +135,9 @@ export class DehydrationManager {
// dehydrate a new device. If it's the same, we can keep the same
// device. (Assume that keyInfo and deviceDisplayName will be the
// same if the key is the same.)
let matches: boolean = this.key && key.length == this.key.length;
let matches: boolean = !!this.key && key.length == this.key.length;
for (let i = 0; matches && i < key.length; i++) {
if (key[i] != this.key[i]) {
if (key[i] != this.key![i]) {
matches = false;
}
}
@@ -150,7 +150,7 @@ export class DehydrationManager {
}
/** returns the device id of the newly created dehydrated device */
public async dehydrateDevice(): Promise<string> {
public async dehydrateDevice(): Promise<string | undefined> {
if (this.inProgress) {
logger.log("Dehydration already in progress -- not starting new dehydration");
return;
@@ -164,7 +164,7 @@ export class DehydrationManager {
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto.cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
@@ -174,7 +174,7 @@ export class DehydrationManager {
{
keyInfo: this.keyInfo,
key,
deviceDisplayName: this.deviceDisplayName,
deviceDisplayName: this.deviceDisplayName!,
time: Date.now(),
},
);
@@ -197,14 +197,14 @@ export class DehydrationManager {
account.mark_keys_as_published();
// dehydrate the account and store it on the server
const pickledAccount = account.pickle(new Uint8Array(this.key));
const pickledAccount = account.pickle(new Uint8Array(this.key!));
const deviceData: {[props: string]: any} = {
algorithm: DEHYDRATION_ALGORITHM,
account: pickledAccount,
};
if (this.keyInfo.passphrase) {
deviceData.passphrase = this.keyInfo.passphrase;
if (this.keyInfo!.passphrase) {
deviceData.passphrase = this.keyInfo!.passphrase;
}
logger.log("Uploading account to server");
+1 -1
View File
@@ -87,7 +87,7 @@ export class DeviceInfo {
BLOCKED: DeviceVerification.Blocked,
};
public algorithms: string[];
public algorithms: string[] = [];
public keys: Record<string, string> = {};
public verified = DeviceVerification.Unverified;
public known = false;
+124 -138
View File
@@ -23,6 +23,7 @@ limitations under the License.
import anotherjson from "another-json";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { EventType } from "../@types/event";
import { TypedReEmitter } from '../ReEmitter';
import { logger } from '../logger';
@@ -198,8 +199,8 @@ export interface IEventDecryptionResult {
}
export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
getRequest(event: MatrixEvent): VerificationRequest | undefined;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined;
setRequest(event: MatrixEvent, request: VerificationRequest): void;
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
}
@@ -274,7 +275,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private trustCrossSignedDevices = true;
// the last time we did a check for the number of one-time-keys on the server.
private lastOneTimeKeyCheck: number = null;
private lastOneTimeKeyCheck: number | null = null;
private oneTimeKeyCheckInProgress = false;
// EncryptionAlgorithm instance for each room
@@ -301,7 +302,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// track if an initial tracking of all the room members
// has happened for a given room. This is delayed
// to avoid loading room members as long as possible.
private roomDeviceTrackingState: Record<string, Promise<void>> = {}; // roomId: Promise<void
private roomDeviceTrackingState: { [roomId: string]: Promise<void> } = {};
// The timestamp of the last time we forced establishment
// of a new session for each device, in milliseconds.
@@ -317,8 +318,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// processing the response.
private sendKeyRequestsImmediately = false;
private oneTimeKeyCount: number;
private needsNewFallback: boolean;
private oneTimeKeyCount?: number;
private needsNewFallback?: boolean;
private fallbackCleanup?: ReturnType<typeof setTimeout>;
/**
@@ -399,8 +400,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// store the fixed version
const fixedKey = fixBackupKey(storedKey);
if (fixedKey) {
const [keyId] = await this.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
const keys = await this.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
}
return olmlib.decodeBase64(fixedKey || storedKey);
@@ -468,8 +469,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await this.deviceList.load();
// build our device keys: these will later be uploaded
this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key;
this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key;
this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key!;
this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key!;
logger.log("Crypto: fetching own devices...");
let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId);
@@ -547,7 +548,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
!deviceTrust.isLocallyVerified() &&
deviceTrust.isCrossSigningVerified()
) {
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId)!;
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
}
}
@@ -587,7 +588,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
privateKey,
};
} finally {
if (decryption) decryption.free();
decryption?.free();
}
}
@@ -695,9 +696,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
// Cross-sign own device
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
builder.addKeySignature(this.userId, this.deviceId, deviceSignature);
builder.addKeySignature(this.userId, this.deviceId, deviceSignature!);
// Sign message key backup with cross-signing master key
if (this.backupManager.backupInfo) {
@@ -763,7 +764,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
) {
const secretStorage = new SecretStorage(
builder.accountDataClientAdapter,
builder.ssssCryptoCallbacks);
builder.ssssCryptoCallbacks,
undefined,
);
if (await secretStorage.hasKey()) {
logger.log("Storing new cross-signing private keys in secret storage");
// This is writing to in-memory account data in
@@ -835,13 +838,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const secretStorage = new SecretStorage(
builder.accountDataClientAdapter,
builder.ssssCryptoCallbacks,
undefined,
);
// the ID of the new SSSS key, if we create one
let newKeyId = null;
let newKeyId: string | null = null;
// create a new SSSS key and set it as default
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey: Uint8Array) => {
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array) => {
if (privateKey) {
opts.key = privateKey;
}
@@ -859,7 +863,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
if (!keyInfo.mac) {
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey(
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
{ keys: { [keyId]: keyInfo } }, "",
);
if (key) {
@@ -934,7 +938,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase?.();
// create a new SSSS key and use the backup key as the new SSSS key
const opts = {} as IAddSecretStorageKeyOpts;
@@ -955,7 +959,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]);
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey!), [newKeyId]);
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross-signing key so the key backup can
@@ -1025,8 +1029,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// in secret storage
const fixedBackupKey = fixBackupKey(sessionBackupKey);
if (fixedBackupKey) {
const keyId = newKeyId || oldKeyId;
await secretStorage.store("m.megolm_backup.v1",
fixedBackupKey, [newKeyId || oldKeyId],
fixedBackupKey, keyId ? [keyId] : null,
);
}
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
@@ -1036,7 +1041,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
// the cache or the user can provide one, and if so, write it to SSSS
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase?.();
if (!backupKey) {
// This will require user intervention to recover from since we don't have the key
// backup key anywhere. The user should probably just set up a new key backup and
@@ -1061,16 +1066,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public addSecretStorageKey(
algorithm: string,
opts: IAddSecretStorageKeyOpts,
keyID: string,
keyID?: string,
): Promise<SecretStorageKeyObject> {
return this.secretStorage.addKey(algorithm, opts, keyID);
}
public hasSecretStorageKey(keyID: string): Promise<boolean> {
public hasSecretStorageKey(keyID?: string): Promise<boolean> {
return this.secretStorage.hasKey(keyID);
}
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> {
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple | null> {
return this.secretStorage.getKey(keyID);
}
@@ -1078,7 +1083,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.store(name, secret, keys);
}
public getSecret(name: string): Promise<string> {
public getSecret(name: string): Promise<string | undefined> {
return this.secretStorage.get(name);
}
@@ -1115,14 +1120,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {boolean} true if the key matches, otherwise false
*/
public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
let decryption = null;
let decryption: PkDecryption | null = null;
try {
decryption = new global.Olm.PkDecryption();
const gotPubkey = decryption.init_with_private_key(privateKey);
// make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey;
} finally {
if (decryption) decryption.free();
decryption?.free();
}
}
@@ -1188,14 +1193,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {boolean} true if the key matches, otherwise false
*/
public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
let signing = null;
let signing: PkSigning | null = null;
try {
signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privateKey);
// make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey;
} finally {
if (signing) signing.free();
signing?.free();
}
}
@@ -1209,14 +1214,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.info("Starting cross-signing key change post-processing");
// sign the current device with the new key, and upload to the server
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({ shouldEmit = false }) => {
return this.baseApis.uploadKeySignatures({
[this.userId]: {
[this.deviceId]: signedDevice,
[this.deviceId]: signedDevice!,
},
}).then((response) => {
const { failures } = response || {};
@@ -1267,9 +1272,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (usersToUpgrade) {
for (const userId of usersToUpgrade) {
if (userId in users) {
await this.baseApis.setDeviceVerified(
userId, users[userId].crossSigningInfo.getId(),
);
await this.baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId()!);
}
}
}
@@ -1296,7 +1299,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private async checkForDeviceVerificationUpgrade(
userId: string,
crossSigningInfo: CrossSigningInfo,
): Promise<IDeviceVerificationUpgrade> {
): Promise<IDeviceVerificationUpgrade | undefined> {
// only upgrade if this is the first cross-signing key that we've seen for
// them, and if their cross-signing key isn't already verified
const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo);
@@ -1359,7 +1362,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @returns {string} the key ID
*/
public getCrossSigningId(type: string): string {
public getCrossSigningId(type: string): string | null {
return this.crossSigningInfo.getId(type);
}
@@ -1370,7 +1373,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @returns {CrossSigningInfo} the cross signing information for the user.
*/
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo {
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
return this.deviceList.getStoredCrossSigningForUser(userId);
}
@@ -1410,8 +1413,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @returns {DeviceTrustLevel}
*/
public checkDeviceInfoTrust(userId: string, device: DeviceInfo): DeviceTrustLevel {
const trustedLocally = !!(device && device.isVerified());
public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel {
const trustedLocally = !!device?.isVerified();
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
if (device && userCrossSigning) {
@@ -1436,13 +1439,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
const device = this.deviceList.getStoredDevice(this.userId, deviceId);
if (!device) return false;
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
return userCrossSigning.checkDeviceTrust(
return userCrossSigning?.checkDeviceTrust(
userCrossSigning,
device,
false,
true,
).isCrossSigningVerified();
).isCrossSigningVerified() ?? false;
}
/*
@@ -1494,7 +1498,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* 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.
*/
async checkOwnCrossSigningTrust({
public async checkOwnCrossSigningTrust({
allowPrivateKeyRequests = false,
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
const userId = this.userId;
@@ -1520,7 +1524,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
const seenPubkey = newCrossSigning.getId();
const seenPubkey = newCrossSigning.getId()!;
const masterChanged = this.crossSigningInfo.getId() !== seenPubkey;
const masterExistsNotLocallyCached =
newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
@@ -1532,18 +1536,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(masterChanged || masterExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing master private key");
let signing = null;
let signing: PkSigning | null = null;
// It's important for control flow that we leave any errors alone for
// higher levels to handle so that e.g. cancelling access properly
// aborts any larger operation as well.
try {
const ret = await this.crossSigningInfo.getCrossSigningKey(
'master', seenPubkey,
);
const ret = await this.crossSigningInfo.getCrossSigningKey('master', seenPubkey);
signing = ret[1];
logger.info("Got cross-signing master private key");
} finally {
if (signing) signing.free();
signing?.free();
}
}
@@ -1575,22 +1577,20 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(selfSigningChanged || selfSigningExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing self-signing private key");
let signing = null;
let signing: PkSigning | null = null;
try {
const ret = await this.crossSigningInfo.getCrossSigningKey(
"self_signing", newCrossSigning.getId("self_signing"),
"self_signing", newCrossSigning.getId("self_signing")!,
);
signing = ret[1];
logger.info("Got cross-signing self-signing private key");
} finally {
if (signing) signing.free();
signing?.free();
}
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
const signedDevice = await this.crossSigningInfo.signDevice(
this.userId, device,
);
keySignatures[this.deviceId] = signedDevice;
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
keySignatures[this.deviceId] = signedDevice!;
}
if (userSigningChanged) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
@@ -1600,26 +1600,26 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(userSigningChanged || userSigningExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing user-signing private key");
let signing = null;
let signing: PkSigning | null = null;
try {
const ret = await this.crossSigningInfo.getCrossSigningKey(
"user_signing", newCrossSigning.getId("user_signing"),
"user_signing", newCrossSigning.getId("user_signing")!,
);
signing = ret[1];
logger.info("Got cross-signing user-signing private key");
} finally {
if (signing) signing.free();
signing?.free();
}
}
if (masterChanged) {
const masterKey = this.crossSigningInfo.keys.master;
await this.signObject(masterKey);
const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId];
const deviceSig = masterKey.signatures![this.userId]["ed25519:" + this.deviceId];
// Include only the _new_ device signature in the upload.
// We may have existing signatures from deleted devices, which will cause
// the entire upload to fail.
keySignatures[this.crossSigningInfo.getId()] = Object.assign(
keySignatures[this.crossSigningInfo.getId()!] = Object.assign(
{} as ISignedKey,
masterKey,
{
@@ -1679,7 +1679,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @param {object} keys The new trusted set of keys
*/
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey>): Promise<void> {
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
if (keys) {
this.crossSigningInfo.setKeys(keys);
} else {
@@ -1721,9 +1721,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
},
});
if (usersToUpgrade.includes(userId)) {
await this.baseApis.setDeviceVerified(
userId, crossSigningInfo.getId(),
);
await this.baseApis.setDeviceVerified(userId, crossSigningInfo.getId()!);
}
}
}
@@ -1771,7 +1769,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @return {string} base64-encoded ed25519 key.
*/
public getDeviceEd25519Key(): string {
public getDeviceEd25519Key(): string | null {
return this.olmDevice.deviceEd25519Key;
}
@@ -1780,7 +1778,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @return {string} base64-encoded curve25519 key.
*/
public getDeviceCurve25519Key(): string {
public getDeviceCurve25519Key(): string | null {
return this.olmDevice.deviceCurve25519Key;
}
@@ -1859,11 +1857,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
public setNeedsNewFallback(needsNewFallback: boolean) {
this.needsNewFallback = !!needsNewFallback;
this.needsNewFallback = needsNewFallback;
}
public getNeedsNewFallback(): boolean {
return this.needsNewFallback;
return !!this.needsNewFallback;
}
// check if it's time to upload one-time keys, and do so if so.
@@ -1983,10 +1981,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
// returns a promise which resolves to the response
private async uploadOneTimeKeys() {
const promises = [];
private async uploadOneTimeKeys(): Promise<IKeysUploadResponse> {
const promises: Promise<unknown>[] = [];
let fallbackJson: Record<string, IOneTimeKey>;
let fallbackJson: Record<string, IOneTimeKey> | undefined;
if (this.getNeedsNewFallback()) {
fallbackJson = {};
const fallbackKeys = await this.olmDevice.getFallbackKey();
@@ -2045,7 +2043,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* module:crypto/deviceinfo|DeviceInfo}.
*/
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
return this.deviceList.downloadKeys(userIds, forceDownload);
return this.deviceList.downloadKeys(userIds, !!forceDownload);
}
/**
@@ -2114,17 +2112,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public async setDeviceVerification(
userId: string,
deviceId: string,
verified?: boolean,
blocked?: boolean,
known?: boolean,
verified: boolean | null = null,
blocked: boolean | null = null,
known: boolean | null = null,
keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo> {
// get rid of any `undefined`s here so we can just check
// for null rather than null or undefined
if (verified === undefined) verified = null;
if (blocked === undefined) blocked = null;
if (known === undefined) known = null;
// 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.
@@ -2235,14 +2227,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.info("Own device " + deviceId + " marked verified: signing");
// Signing only needed if other device not already signed
let device: ISignedKey;
let device: ISignedKey | undefined;
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
if (deviceTrust.isCrossSigningVerified()) {
logger.log(`Own device ${deviceId} already cross-signing verified`);
} else {
device = await this.crossSigningInfo.signDevice(
device = (await this.crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
))!;
}
if (device) {
@@ -2250,7 +2242,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.info("Uploading signature for " + deviceId);
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
[deviceId]: device!,
},
});
const { failures } = response || {};
@@ -2276,7 +2268,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return deviceObj;
}
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest {
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
return this.inRoomVerificationRequests.findRequestInProgress(roomId);
}
@@ -2293,7 +2285,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
}
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
}
@@ -2332,9 +2324,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
method: string,
userId: string,
deviceId: string,
transactionId: string = null,
transactionId: string | null = null,
): VerificationBase<any, any> {
let request: Request;
let request: Request | undefined;
if (transactionId) {
request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
if (!request) {
@@ -2478,7 +2470,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
const ret: Partial<IEncryptedEventInfo> = {};
ret.senderKey = event.getSenderKey();
ret.senderKey = event.getSenderKey() ?? undefined;
ret.algorithm = event.getWireContent().algorithm;
if (!ret.senderKey || !ret.algorithm) {
@@ -2499,7 +2491,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// was sent from. In the case of Megolm, it's actually the Curve25519
// identity key of the device which set up the Megolm session.
ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey);
ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey) ?? undefined;
// so far so good, but now we need to check that the sender of this event
// hadn't advertised someone else's Curve25519 key as their own. We do that
@@ -2597,7 +2589,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// because it first stores in memory. We should await the promise only
// after all the in-memory state (roomEncryptors and _roomList) has been updated
// to avoid races when calling this method multiple times. Hence keep a hold of the promise.
let storeConfigPromise: Promise<void> = null;
let storeConfigPromise: Promise<void> | null = null;
if (!existingConfig) {
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
}
@@ -2666,7 +2658,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!promise) {
promise = trackMembers();
this.roomDeviceTrackingState[roomId] = promise.catch(err => {
this.roomDeviceTrackingState[roomId] = null;
delete this.roomDeviceTrackingState[roomId];
throw err;
});
}
@@ -2727,9 +2719,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
if (s === null) return;
const sess = this.olmDevice.exportInboundGroupSession(
s.senderKey, s.sessionId, s.sessionData,
);
const sess = this.olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData!);
delete sess.first_known_index;
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
exportedSessions.push(sess);
@@ -2754,7 +2744,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const total = keys.length;
function updateProgress() {
opts.progressCallback({
opts.progressCallback?.({
stage: "load_keys",
successes,
failures,
@@ -2809,12 +2799,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
public async encryptEvent(event: MatrixEvent, room?: Room): Promise<void> {
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
const roomId = event.getRoomId();
const roomId = event.getRoomId()!;
const alg = this.roomEncryptors.get(roomId);
if (!alg) {
@@ -2862,8 +2852,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
event.makeEncrypted(
"m.room.encrypted",
encryptedContent,
this.olmDevice.deviceCurve25519Key,
this.olmDevice.deviceEd25519Key,
this.olmDevice.deviceCurve25519Key!,
this.olmDevice.deviceEd25519Key!,
);
}
@@ -2896,7 +2886,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
};
} else {
const content = event.getWireContent();
const alg = this.getRoomDecryptor(event.getRoomId(), content.algorithm);
const alg = this.getRoomDecryptor(event.getRoomId()!, content.algorithm);
return alg.decryptEvent(event);
}
}
@@ -2981,7 +2971,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {module:models/event.MatrixEvent} event encryption event
*/
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
const roomId = event.getRoomId();
const roomId = event.getRoomId()!;
const content = event.getContent<IRoomEncryption>();
try {
@@ -2989,8 +2979,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// finished processing the sync, in onSyncCompleted.
await this.setRoomEncryption(roomId, content, true);
} catch (e) {
logger.error("Error configuring encryption in room " + roomId +
":", e);
logger.error(`Error configuring encryption in room ${roomId}`, e);
}
}
@@ -3024,7 +3013,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {Object} syncData the data from the 'MatrixClient.sync' event
*/
public async onSyncCompleted(syncData: ISyncStateData): Promise<void> {
this.deviceList.setSyncToken(syncData.nextSyncToken);
this.deviceList.setSyncToken(syncData.nextSyncToken ?? null);
this.deviceList.saveIfDirty();
// we always track our own device list (for key backups etc)
@@ -3086,7 +3075,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {string[]} List of user IDs
*/
private async getTrackedE2eUsers(): Promise<string[]> {
const e2eUserIds = [];
const e2eUserIds: string[] = [];
for (const room of this.getTrackedE2eRooms()) {
const members = await room.getEncryptionTargetMembers();
for (const member of members) {
@@ -3143,7 +3132,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const deviceId = deviceInfo.deviceId;
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
};
@@ -3306,7 +3295,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = (event: MatrixEvent) => {
const createRequest = (event: MatrixEvent): VerificationRequest | undefined => {
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
return;
}
@@ -3315,7 +3304,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!deviceId) {
return;
}
const userId = event.getSender();
const userId = event.getSender()!;
const channel = new ToDeviceChannel(
this.baseApis,
userId,
@@ -3348,10 +3337,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
const createRequest = (event: MatrixEvent) => {
const channel = new InRoomChannel(
this.baseApis,
event.getRoomId(),
);
const channel = new InRoomChannel(this.baseApis, event.getRoomId()!);
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
};
@@ -3361,15 +3347,15 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private async handleVerificationEvent(
event: MatrixEvent,
requestsMap: IRequestsMap,
createRequest: (event: MatrixEvent) => VerificationRequest,
createRequest: (event: MatrixEvent) => VerificationRequest | undefined,
isLiveEvent = true,
): Promise<void> {
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
if (event.isSending() && event.status != EventStatus.SENT) {
let eventIdListener;
let statusListener;
let eventIdListener: () => void;
let statusListener: () => void;
try {
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
eventIdListener = resolve;
statusListener = () => {
if (event.status == EventStatus.CANCELLED) {
@@ -3383,11 +3369,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.error("error while waiting for the verification event to be sent: ", err);
return;
} finally {
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.removeListener(MatrixEventEvent.Status, statusListener);
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener!);
event.removeListener(MatrixEventEvent.Status, statusListener!);
}
}
let request = requestsMap.getRequest(event);
let request: VerificationRequest | undefined = requestsMap.getRequest(event);
let isNewRequest = false;
if (!request) {
request = createRequest(event);
@@ -3550,13 +3536,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// this way we don't start device queries after sync on behalf of this room which we won't use
// the result of anyway, as we'll need to do a query again once all the members are fetched
// by calling _trackRoomDevices
if (this.roomDeviceTrackingState[roomId]) {
if (roomId in this.roomDeviceTrackingState) {
if (member.membership == 'join') {
logger.log('Join event for ' + member.userId + ' in ' + roomId);
// make sure we are tracking the deviceList for this user
this.deviceList.startTrackingDeviceList(member.userId);
} else if (member.membership == 'invite' &&
this.clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) {
this.clientStore.getRoom(roomId)?.shouldEncryptForInvitedMembers()
) {
logger.log('Invite event for ' + member.userId + ' in ' + roomId);
this.deviceList.startTrackingDeviceList(member.userId);
}
@@ -3646,7 +3633,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
logger.debug(`room key request for unencrypted room ${roomId}`);
return;
}
const encryptor = this.roomEncryptors.get(roomId);
const encryptor = this.roomEncryptors.get(roomId)!;
const device = this.deviceList.getStoredDevice(userId, deviceId);
if (!device) {
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
@@ -3654,7 +3641,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
try {
await encryptor.reshareKeyWithDevice(body.sender_key, body.session_id, userId, device);
await encryptor.reshareKeyWithDevice!(body.sender_key, body.session_id, userId, device);
} catch (e) {
logger.warn(
"Failed to re-share keys for session " + body.session_id +
@@ -3687,7 +3674,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
const decryptor = this.roomDecryptors.get(roomId).get(alg);
const decryptor = this.roomDecryptors.get(roomId)!.get(alg);
if (!decryptor) {
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
return;
@@ -3752,11 +3739,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @raises {module:crypto.algorithms.DecryptionError} if the algorithm is
* unknown
*/
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
let decryptors: Map<string, DecryptionAlgorithm>;
let alg: DecryptionAlgorithm;
public getRoomDecryptor(roomId: string | null, algorithm: string): DecryptionAlgorithm {
let decryptors: Map<string, DecryptionAlgorithm> | undefined;
let alg: DecryptionAlgorithm | undefined;
roomId = roomId || null;
if (roomId) {
decryptors = this.roomDecryptors.get(roomId);
if (!decryptors) {
@@ -3782,7 +3768,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
crypto: this,
olmDevice: this.olmDevice,
baseApis: this.baseApis,
roomId: roomId,
roomId: roomId ?? undefined,
});
if (decryptors) {
@@ -3799,10 +3785,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @return {array} An array of room decryptors
*/
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
const decryptors = [];
const decryptors: DecryptionAlgorithm[] = [];
for (const d of this.roomDecryptors.values()) {
if (d.has(algorithm)) {
decryptors.push(d.get(algorithm));
decryptors.push(d.get(algorithm)!);
}
}
return decryptors;
@@ -3839,7 +3825,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* key will be returned. Otherwise null will be returned.
*
*/
export function fixBackupKey(key: string): string | null {
export function fixBackupKey(key?: string): string | null {
if (typeof key !== "string" || key.indexOf(",") < 0) {
return null;
}
@@ -3876,7 +3862,7 @@ export class IncomingRoomKeyRequest {
constructor(event: MatrixEvent) {
const content = event.getContent();
this.userId = event.getSender();
this.userId = event.getSender()!;
this.deviceId = content.requesting_device_id;
this.requestId = content.request_id;
this.requestBody = content.body || {};
@@ -3901,7 +3887,7 @@ class IncomingRoomKeyRequestCancellation {
constructor(event: MatrixEvent) {
const content = event.getContent();
this.userId = event.getSender();
this.userId = event.getSender()!;
this.deviceId = content.requesting_device_id;
this.requestId = content.request_id;
}
+6 -18
View File
@@ -21,7 +21,6 @@ limitations under the License.
*/
import anotherjson from "another-json";
import { Logger } from "loglevel";
import type { PkSigning } from "@matrix-org/olm";
import { OlmDevice } from "./OlmDevice";
@@ -56,7 +55,7 @@ export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
export interface IOlmSessionResult {
device: DeviceInfo;
sessionId?: string;
sessionId: string | null;
}
/**
@@ -137,7 +136,7 @@ export async function encryptMessageForDevice(
interface IExistingOlmSession {
device: DeviceInfo;
sessionId?: string;
sessionId: string | null;
}
/**
@@ -225,19 +224,8 @@ export async function ensureOlmSessionsForDevices(
force = false,
otkTimeout?: number,
failedServers?: string[],
log: Logger = logger,
log = logger,
): Promise<Record<string, Record<string, IOlmSessionResult>>> {
if (typeof force === "number") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
log = failedServers;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
const devicesWithoutSession: [string, string][] = [
// [userId, deviceId], ...
];
@@ -365,7 +353,7 @@ export async function ensureOlmSessionsForDevices(
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey: IOneTimeKey = null;
let oneTimeKey: IOneTimeKey | null = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
@@ -388,7 +376,7 @@ export async function ensureOlmSessionsForDevices(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
if (resolveSession[key]) {
resolveSession[key](sid);
resolveSession[key](sid ?? undefined);
}
result[userId][deviceId].sessionId = sid;
}, (e) => {
@@ -413,7 +401,7 @@ async function _verifyKeyAndStartSession(
oneTimeKey: IOneTimeKey,
userId: string,
deviceInfo: DeviceInfo,
): Promise<string> {
): Promise<string | null> {
const deviceId = deviceInfo.deviceId;
try {
await verifySignature(
+2 -2
View File
@@ -20,7 +20,7 @@ import * as bs58 from 'bs58';
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key: ArrayLike<number>): string {
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
@@ -32,7 +32,7 @@ export function encodeRecoveryKey(key: ArrayLike<number>): string {
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g).join(" ");
return base58key.match(/.{1,4}/g)?.join(" ");
}
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
+2 -2
View File
@@ -90,14 +90,14 @@ export interface CryptoStore {
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
func: (session: ISessionInfo | null) => void,
): void;
getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
@@ -307,7 +307,7 @@ export class Backend implements CryptoStore {
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result: OutgoingRoomKeyRequest = null;
let result: OutgoingRoomKeyRequest | null = null;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
@@ -375,7 +375,7 @@ export class Backend implements CryptoStore {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -395,7 +395,7 @@ export class Backend implements CryptoStore {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -411,7 +411,7 @@ export class Backend implements CryptoStore {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -439,7 +439,7 @@ export class Backend implements CryptoStore {
try {
func(countReq.result);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -465,7 +465,7 @@ export class Backend implements CryptoStore {
try {
func(results);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
}
};
@@ -475,7 +475,7 @@ export class Backend implements CryptoStore {
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
func: (session: ISessionInfo | null) => void,
): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
@@ -490,12 +490,12 @@ export class Backend implements CryptoStore {
func(null);
}
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -508,7 +508,7 @@ export class Backend implements CryptoStore {
func(null);
}
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -537,11 +537,11 @@ export class Backend implements CryptoStore {
fixed,
time: Date.now(),
});
return promiseifyTxn(txn);
await promiseifyTxn(txn);
}
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
let result;
let result: IProblem | null = null;
const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
@@ -604,8 +604,8 @@ export class Backend implements CryptoStore {
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
let session: InboundGroupSessionData | boolean = false;
let withheld: IWithheld | boolean = false;
let session: InboundGroupSessionData | null | boolean = false;
let withheld: IWithheld | null | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
@@ -619,7 +619,7 @@ export class Backend implements CryptoStore {
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
@@ -636,7 +636,7 @@ export class Backend implements CryptoStore {
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -654,14 +654,14 @@ export class Backend implements CryptoStore {
sessionData: cursor.value.session,
});
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
}
};
@@ -678,7 +678,7 @@ export class Backend implements CryptoStore {
senderCurve25519Key, sessionId, session: sessionData,
});
addReq.onerror = (ev) => {
if (addReq.error.name === 'ConstraintError') {
if (addReq.error?.name === 'ConstraintError') {
// This stops the error from triggering the txn's onerror
ev.stopPropagation();
// ...and this stops it from aborting the transaction
@@ -726,7 +726,7 @@ export class Backend implements CryptoStore {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
};
}
@@ -754,7 +754,7 @@ export class Backend implements CryptoStore {
try {
func(rooms);
} catch (e) {
abortWithException(txn, e);
abortWithException(txn, <Error>e);
}
}
};
@@ -1050,7 +1050,7 @@ function abortWithException(txn: IDBTransaction, e: Error) {
}
}
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T> {
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
return new Promise((resolve, reject) => {
txn.oncomplete = () => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
+51 -46
View File
@@ -18,7 +18,7 @@ import { logger, PrefixedLogger } from '../../logger';
import { LocalStorageCryptoStore } from './localStorage-crypto-store';
import { MemoryCryptoStore } from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import { InvalidCryptoStoreError } from '../../errors';
import { InvalidCryptoStoreError, InvalidCryptoStoreState } from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
import {
CryptoStore,
@@ -64,8 +64,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
private backendPromise: Promise<CryptoStore> = null;
private backend: CryptoStore = null;
private backendPromise?: Promise<CryptoStore>;
private backend?: CryptoStore;
/**
* Create a new IndexedDBCryptoStore
@@ -141,7 +141,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data
// in this db so we should use it or nothing at all.
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
}
logger.warn(
`unable to connect to indexeddb ${this.dbName}` +
@@ -213,7 +213,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* same instance as passed in, or the existing one.
*/
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
return this.backend.getOrAddOutgoingRoomKeyRequest(request);
return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -227,7 +227,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* not found
*/
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequest(requestBody);
return this.backend!.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -241,7 +241,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* requests in those states, an arbitrary one is chosen.
*/
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequestByState(wantedStates);
return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
@@ -252,7 +252,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @return {Promise<Array<*>>} Returns an array of requests in the given state
*/
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
}
/**
@@ -270,7 +270,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getOutgoingRoomKeyRequestsByTarget(
return this.backend!.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
@@ -292,7 +292,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.updateOutgoingRoomKeyRequest(
return this.backend!.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
@@ -310,7 +310,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -323,7 +323,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
this.backend.getAccount(txn, func);
this.backend!.getAccount(txn, func);
}
/**
@@ -334,7 +334,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} accountPickle The new account pickle to store.
*/
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend.storeAccount(txn, accountPickle);
this.backend!.storeAccount(txn, accountPickle);
}
/**
@@ -349,7 +349,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void {
this.backend.getCrossSigningKeys(txn, func);
this.backend!.getCrossSigningKeys(txn, func);
}
/**
@@ -362,7 +362,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
this.backend!.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -372,7 +372,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} keys keys object as getCrossSigningKeys()
*/
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this.backend.storeCrossSigningKeys(txn, keys);
this.backend!.storeCrossSigningKeys(txn, keys);
}
/**
@@ -387,7 +387,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.backend.storeSecretStorePrivateKey(txn, type, key);
this.backend!.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -398,7 +398,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(int)} func Called with the count of sessions
*/
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend.countEndToEndSessions(txn, func);
this.backend!.countEndToEndSessions(txn, func);
}
/**
@@ -417,9 +417,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
func: (session: ISessionInfo | null) => void,
): void {
this.backend.getEndToEndSession(deviceKey, sessionId, txn, func);
this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -438,7 +438,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSessions(deviceKey, txn, func);
this.backend!.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -448,8 +448,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
this.backend.getAllEndToEndSessions(txn, func);
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
this.backend!.getAllEndToEndSessions(txn, func);
}
/**
@@ -465,19 +465,19 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
}
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.backend.getEndToEndSessionProblem(deviceKey, timestamp);
return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
}
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this.backend.filterOutNotifiedErrorDevices(devices);
return this.backend!.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -497,7 +497,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
}
/**
@@ -511,7 +511,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
txn: IDBTransaction,
func: (session: ISession | null) => void,
): void {
this.backend.getAllEndToEndInboundGroupSessions(txn, func);
this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -529,7 +529,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
/**
@@ -547,7 +547,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
public storeEndToEndInboundGroupSessionWithheld(
@@ -556,7 +556,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionData: IWithheld,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
}
// End-to-end device tracking
@@ -572,7 +572,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this.backend.storeEndToEndDeviceData(deviceData, txn);
this.backend!.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -583,7 +583,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* device data
*/
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
this.backend.getEndToEndDeviceData(txn, func);
this.backend!.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -595,7 +595,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this.backend.storeEndToEndRoom(roomId, roomInfo, txn);
this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -604,7 +604,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this.backend.getEndToEndRooms(txn, func);
this.backend!.getEndToEndRooms(txn, func);
}
// session backups
@@ -616,7 +616,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves to an array of inbound group sessions
*/
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this.backend.getSessionsNeedingBackup(limit);
return this.backend!.getSessionsNeedingBackup(limit);
}
/**
@@ -625,7 +625,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves to the number of sessions
*/
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this.backend.countSessionsNeedingBackup(txn);
return this.backend!.countSessionsNeedingBackup(txn);
}
/**
@@ -635,7 +635,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves when the sessions are unmarked
*/
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.unmarkSessionsNeedingBackup(sessions, txn);
return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -645,7 +645,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @returns {Promise} resolves when the sessions are marked
*/
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.markSessionsNeedingBackup(sessions, txn);
return this.backend!.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -661,7 +661,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
sessionId: string,
txn?: IDBTransaction,
): void {
this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
}
/**
@@ -674,7 +674,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
}
/**
@@ -685,7 +685,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
parkedData: ParkedSharedHistory,
txn?: IDBTransaction,
): void {
this.backend.addParkedSharedHistory(roomId, parkedData, txn);
this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
}
/**
@@ -695,7 +695,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
roomId: string,
txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> {
return this.backend.takeParkedSharedHistory(roomId, txn);
return this.backend!.takeParkedSharedHistory(roomId, txn);
}
/**
@@ -720,7 +720,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise<T> {
return this.backend.doTxn(mode, stores, func, log);
public doTxn<T>(
mode: Mode,
stores: Iterable<string>,
func: (txn: IDBTransaction) => T,
log?: PrefixedLogger,
): Promise<T> {
return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
}
}
+12 -12
View File
@@ -69,7 +69,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public static exists(store: Storage): boolean {
const length = store.length;
for (let i = 0; i < length; i++) {
if (store.key(i).startsWith(E2E_PREFIX)) {
if (store.key(i)?.startsWith(E2E_PREFIX)) {
return true;
}
}
@@ -85,7 +85,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
let count = 0;
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) ++count;
}
func(count);
}
@@ -129,8 +129,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i)!.split('/')[1];
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
func(sess);
}
@@ -220,7 +220,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
@@ -229,7 +229,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func({
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
sessionData: getJsonItem(this.store, key)!,
});
}
}
@@ -297,9 +297,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
if (key?.startsWith(prefix)) {
const roomId = key.slice(prefix.length);
result[roomId] = getJsonItem(this.store, key);
result[roomId] = getJsonItem(this.store, key)!;
}
}
func(result);
@@ -320,7 +320,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
sessions.push({
senderKey: senderKey,
sessionId: sessionId,
sessionData: sessionData,
sessionData: sessionData!,
});
},
);
@@ -417,10 +417,10 @@ function getJsonItem<T>(store: Storage, key: string): T | null {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
return JSON.parse(store.getItem(key)!);
} catch (e) {
logger.log("Error: Failed to get key %s: %s", key, e.stack || e);
logger.log(e.stack);
logger.log("Error: Failed to get key %s: %s", key, (<Error>e).message);
logger.log((<Error>e).stack);
}
return null;
}
+1 -1
View File
@@ -54,7 +54,7 @@ export class MemoryCryptoStore implements CryptoStore {
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
// Opaque device data object
private deviceData: IDeviceData = null;
private deviceData: IDeviceData | null = null;
private rooms: { [roomId: string]: IRoomEncryption } = {};
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
+25 -24
View File
@@ -21,6 +21,7 @@ limitations under the License.
*/
import { MatrixEvent } from '../../models/event';
import { EventType } from '../../@types/event';
import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo';
import { newTimeoutError } from "./Error";
@@ -33,7 +34,7 @@ import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(public readonly startEvent: MatrixEvent) {
constructor(public readonly startEvent: MatrixEvent | null) {
super();
}
}
@@ -54,14 +55,14 @@ export class VerificationBase<
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
private cancelled = false;
private _done = false;
private promise: Promise<void> = null;
private transactionTimeoutTimer: ReturnType<typeof setTimeout> = null;
protected expectedEvent: string;
private resolve: () => void;
private reject: (e: Error | MatrixEvent) => void;
private resolveEvent: (e: MatrixEvent) => void;
private rejectEvent: (e: Error) => void;
private started: boolean;
private promise: Promise<void> | null = null;
private transactionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
protected expectedEvent?: string;
private resolve?: () => void;
private reject?: (e: Error | MatrixEvent) => void;
private resolveEvent?: (e: MatrixEvent) => void;
private rejectEvent?: (e: Error) => void;
private started?: boolean;
/**
* Base class for verification methods.
@@ -95,7 +96,7 @@ export class VerificationBase<
public readonly baseApis: MatrixClient,
public readonly userId: string,
public readonly deviceId: string,
public startEvent: MatrixEvent,
public startEvent: MatrixEvent | null,
public readonly request: VerificationRequest,
) {
super();
@@ -182,13 +183,13 @@ export class VerificationBase<
} else if (e.getType() === this.expectedEvent) {
// if we receive an expected m.key.verification.done, then just
// ignore it, since we don't need to do anything about it
if (this.expectedEvent !== "m.key.verification.done") {
if (this.expectedEvent !== EventType.KeyVerificationDone) {
this.expectedEvent = undefined;
this.rejectEvent = undefined;
this.resetTimer();
this.resolveEvent(e);
this.resolveEvent?.(e);
}
} else if (e.getType() === "m.key.verification.cancel") {
} else if (e.getType() === EventType.KeyVerificationCancel) {
const reject = this.reject;
this.reject = undefined;
// there is only promise to reject if verify has been called
@@ -217,11 +218,11 @@ export class VerificationBase<
}
}
public done(): Promise<KeysDuringVerification | void> {
public async done(): Promise<KeysDuringVerification | void> {
this.endTimer(); // always kill the activity timer
if (!this._done) {
this.request.onVerifierFinished();
this.resolve();
this.resolve?.();
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
}
}
@@ -241,20 +242,20 @@ export class VerificationBase<
const sender = e.getSender();
if (sender !== this.userId) {
const content = e.getContent();
if (e.getType() === "m.key.verification.cancel") {
if (e.getType() === EventType.KeyVerificationCancel) {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
this.send("m.key.verification.cancel", content);
this.send(EventType.KeyVerificationCancel, content);
} else {
this.send("m.key.verification.cancel", {
this.send(EventType.KeyVerificationCancel, {
code: "m.unknown",
reason: content.body || "Unknown reason",
});
}
}
} else {
this.send("m.key.verification.cancel", {
this.send(EventType.KeyVerificationCancel, {
code: "m.unknown",
reason: e.toString(),
});
@@ -290,7 +291,7 @@ export class VerificationBase<
this.endTimer();
resolve(...args);
};
this.reject = (e: Error) => {
this.reject = (e: Error | MatrixEvent) => {
this._done = true;
this.endTimer();
reject(e);
@@ -300,12 +301,12 @@ export class VerificationBase<
this.started = true;
this.resetTimer(); // restart the timeout
new Promise<void>((resolve, reject) => {
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
if (crossSignId === this.deviceId) {
reject(new Error("Device ID is the same as the cross-signing ID"));
}
resolve();
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
}).then(() => this.doVerification!()).then(this.done.bind(this), this.cancel.bind(this));
}
return this.promise;
}
@@ -325,7 +326,7 @@ export class VerificationBase<
verifier(keyId, device, keyInfo);
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
} else {
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
verifier(keyId, DeviceInfo.fromStorage({
keys: {
@@ -355,7 +356,7 @@ export class VerificationBase<
// to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like.
for (const [deviceId, keyId, key] of verifiedDevices) {
await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
}
// if one of the user's own devices is being marked as verified / unverified,
+3 -2
View File
@@ -21,11 +21,12 @@ limitations under the License.
*/
import { MatrixEvent } from "../../models/event";
import { EventType } from '../../@types/event';
export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
export function newVerificationError(code: string, reason: string, extraData?: Record<string, any>): MatrixEvent {
const content = Object.assign({}, { code, reason }, extraData);
return new MatrixEvent({
type: "m.key.verification.cancel",
type: EventType.KeyVerificationCancel,
content,
});
}
+38 -39
View File
@@ -49,7 +49,7 @@ type EventHandlerMap = {
* @extends {module:crypto/verification/Base}
*/
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
public reciprocateQREvent: IReciprocateQr;
public reciprocateQREvent?: IReciprocateQr;
public static factory(
channel: IVerificationChannel,
@@ -76,7 +76,7 @@ export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
const { qrCodeData } = this.request;
// 1. check the secret
if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) {
if (this.startEvent.getContent()['secret'] !== qrCodeData?.encodedSharedSecret) {
throw newKeyMismatchError();
}
@@ -92,21 +92,21 @@ export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
// 3. determine key to sign / mark as trusted
const keys: Record<string, string> = {};
switch (qrCodeData.mode) {
switch (qrCodeData?.mode) {
case Mode.VerifyOtherUser: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
keys[`ed25519:${masterKey}`] = masterKey!;
break;
}
case Mode.VerifySelfTrusted: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!;
break;
}
case Mode.VerifySelfUntrusted: {
const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
keys[`ed25519:${masterKey}`] = masterKey!;
break;
}
}
@@ -147,7 +147,7 @@ interface IQrData {
prefix: string;
version: number;
mode: Mode;
transactionId: string;
transactionId?: string;
firstKeyB64: string;
secondKeyB64: string;
secretB64: string;
@@ -158,41 +158,41 @@ export class QRCodeData {
public readonly mode: Mode,
private readonly sharedSecret: string,
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
public readonly otherUserMasterKey: string | undefined,
public readonly otherUserMasterKey: string | null,
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
public readonly otherDeviceKey: string | undefined,
public readonly otherDeviceKey: string | null,
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
public readonly myMasterKey: string | undefined,
public readonly myMasterKey: string | null,
private readonly buffer: Buffer,
) {}
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
const sharedSecret = QRCodeData.generateSharedSecret();
const mode = QRCodeData.determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
let otherUserMasterKey: string | null = null;
let otherDeviceKey: string | null = null;
let myMasterKey: string | null = null;
if (mode === Mode.VerifyOtherUser) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo!.getId("master");
} else if (mode === Mode.VerifySelfTrusted) {
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
} else if (mode === Mode.VerifySelfUntrusted) {
const myUserId = client.getUserId();
const myUserId = client.getUserId()!;
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master");
myMasterKey = myCrossSigningInfo!.getId("master");
}
const qrData = QRCodeData.generateQrData(
request, client, mode,
request,
client,
mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
otherUserMasterKey!,
otherDeviceKey!,
myMasterKey!,
);
const buffer = QRCodeData.generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
/**
@@ -213,12 +213,11 @@ export class QRCodeData {
}
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
const myUserId = client.getUserId();
const myUserId = client.getUserId()!;
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = client.getStoredDevice(myUserId, otherDeviceId);
const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined;
if (!device) {
throw new Error("could not find device " + otherDeviceId);
throw new Error("could not find device " + otherDevice?.deviceId);
}
return device.getFingerprint();
}
@@ -245,13 +244,13 @@ export class QRCodeData {
client: MatrixClient,
mode: Mode,
encodedSharedSecret: string,
otherUserMasterKey: string,
otherDeviceKey: string,
myMasterKey: string,
otherUserMasterKey?: string,
otherDeviceKey?: string,
myMasterKey?: string,
): IQrData {
const myUserId = client.getUserId();
const myUserId = client.getUserId()!;
const transactionId = request.channel.transactionId;
const qrData = {
const qrData: IQrData = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
@@ -265,18 +264,18 @@ export class QRCodeData {
if (mode === Mode.VerifyOtherUser) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
qrData.secondKeyB64 = otherUserMasterKey!;
} else if (mode === Mode.VerifySelfTrusted) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
qrData.secondKeyB64 = otherDeviceKey!;
} else if (mode === Mode.VerifySelfUntrusted) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
qrData.firstKeyB64 = client.getDeviceEd25519Key()!;
// Second key is what we think our master cross signing key is
qrData.secondKeyB64 = myMasterKey;
qrData.secondKeyB64 = myMasterKey!;
}
return qrData;
}
+62 -97
View File
@@ -32,13 +32,15 @@ import {
} from './Error';
import { logger } from '../../logger';
import { IContent, MatrixEvent } from "../../models/event";
import { generateDecimalSas } from './SASDecimal';
import { EventType } from '../../@types/event';
const START_TYPE = "m.key.verification.start";
const START_TYPE = EventType.KeyVerificationStart;
const EVENTS = [
"m.key.verification.accept",
"m.key.verification.key",
"m.key.verification.mac",
EventType.KeyVerificationAccept,
EventType.KeyVerificationKey,
EventType.KeyVerificationMac,
];
let olmutil: Utility;
@@ -51,22 +53,6 @@ const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes: number[]): [number, number, number] {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
* +--------+--------+--------+--------+--------+
* bits: 87654321 87654321 87654321 87654321 87654321
* \____________/\_____________/\____________/
* 1st number 2nd number 3rd number
*/
return [
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
];
}
type EmojiMapping = [emoji: string, name: string];
const emojiMapping: EmojiMapping[] = [
@@ -231,7 +217,7 @@ const MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_LIST);
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
return Array.isArray(anArray) ? anArray.filter(x => aSet.has(x)) : [];
}
export enum SasEvent {
@@ -247,10 +233,10 @@ type EventHandlerMap = {
* @extends {module:crypto/verification/Base}
*/
export class SAS extends Base<SasEvent, EventHandlerMap> {
private waitingForAccept: boolean;
public ourSASPubKey: string;
public theirSASPubKey: string;
public sasEvent: ISasEvent;
private waitingForAccept?: boolean;
public ourSASPubKey?: string;
public theirSASPubKey?: string;
public sasEvent?: ISasEvent;
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
@@ -293,7 +279,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
return false;
}
const content = event.getContent();
return content && content.method === SAS.NAME && this.waitingForAccept;
return content?.method === SAS.NAME && !!this.waitingForAccept;
}
private async sendStart(): Promise<Record<string, any>> {
@@ -310,6 +296,45 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
return startContent;
}
private async verifyAndCheckMAC(
keyAgreement: string,
sasMethods: string[],
olmSAS: OlmSAS,
macMethod: string,
): Promise<void> {
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
};
this.emit(SasEvent.ShowSas, this.sasEvent);
});
const [e] = await Promise.all([
this.waitForEvent(EventType.KeyVerificationMac)
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this.expectedEvent = EventType.KeyVerificationDone;
return e;
}),
verifySAS,
]);
const content = e.getContent();
await this.checkMAC(olmSAS, content, macMethod);
}
private async doSendVerification(): Promise<void> {
this.waitingForAccept = true;
let startContent;
@@ -329,7 +354,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
let e;
try {
e = await this.waitForEvent("m.key.verification.accept");
e = await this.waitForEvent(EventType.KeyVerificationAccept);
} finally {
this.waitingForAccept = false;
}
@@ -351,11 +376,11 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
const olmSAS = new global.Olm.SAS();
try {
this.ourSASPubKey = olmSAS.get_pubkey();
await this.send("m.key.verification.key", {
await this.send(EventType.KeyVerificationKey, {
key: this.ourSASPubKey,
});
e = await this.waitForEvent("m.key.verification.key");
e = await this.waitForEvent(EventType.KeyVerificationKey);
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(startContent);
@@ -366,37 +391,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
};
this.emit(SasEvent.ShowSas, this.sasEvent);
});
[e] = await Promise.all([
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this.checkMAC(olmSAS, content, macMethod);
await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
} finally {
olmSAS.free();
}
@@ -405,7 +400,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
private async doRespondVerification(): Promise<void> {
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this.channel.completedContentFromEvent(this.startEvent);
let content = this.channel.completedContentFromEvent(this.startEvent!);
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
@@ -423,7 +418,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
await this.send("m.key.verification.accept", {
await this.send(EventType.KeyVerificationAccept, {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -432,47 +427,17 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
commitment: olmutil.sha256(commitmentStr),
});
let e = await this.waitForEvent("m.key.verification.key");
const e = await this.waitForEvent(EventType.KeyVerificationKey);
// FIXME: make sure event is properly formed
content = e.getContent();
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
this.ourSASPubKey = olmSAS.get_pubkey();
await this.send("m.key.verification.key", {
await this.send(EventType.KeyVerificationKey, {
key: this.ourSASPubKey,
});
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
};
this.emit(SasEvent.ShowSas, this.sasEvent);
});
[e] = await Promise.all([
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this.checkMAC(olmSAS, content, macMethod);
await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
} finally {
olmSAS.free();
}
@@ -480,7 +445,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
const mac = {};
const keyList = [];
const keyList: string[] = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.baseApis.getUserId() + this.baseApis.deviceId
+ this.userId + this.deviceId
@@ -507,7 +472,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
return this.send("m.key.verification.mac", { mac, keys });
return this.send(EventType.KeyVerificationMac, { mac, keys });
}
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Implementation of decimal encoding of SAS as per:
* https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal
* @param sasBytes the five bytes generated by HKDF
* @returns the derived three numbers between 1000 and 9191 inclusive
*/
export function generateDecimalSas(sasBytes: number[]): [number, number, number] {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
* +--------+--------+--------+--------+--------+
* bits: 87654321 87654321 87654321 87654321 87654321
* \____________/\_____________/\____________/
* 1st number 2nd number 3rd number
*/
return [
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
];
}
+2 -2
View File
@@ -19,10 +19,10 @@ import { VerificationRequest } from "./VerificationRequest";
export interface IVerificationChannel {
request?: VerificationRequest;
readonly userId: string;
readonly userId?: string;
readonly roomId?: string;
readonly deviceId?: string;
readonly transactionId: string;
readonly transactionId?: string;
readonly receiveStartFromOtherDevices?: boolean;
getTimestamp(event: MatrixEvent): number;
send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
@@ -37,7 +37,7 @@ const M_RELATES_TO = "m.relates_to";
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export class InRoomChannel implements IVerificationChannel {
private requestEventId: string = null;
private requestEventId?: string;
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
@@ -47,7 +47,7 @@ export class InRoomChannel implements IVerificationChannel {
constructor(
private readonly client: MatrixClient,
public readonly roomId: string,
public userId: string = null,
public userId?: string,
) {
}
@@ -56,11 +56,11 @@ export class InRoomChannel implements IVerificationChannel {
}
/** The transaction id generated/used by this verification channel */
public get transactionId(): string {
public get transactionId(): string | undefined {
return this.requestEventId;
}
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string | undefined {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
@@ -103,12 +103,12 @@ export class InRoomChannel implements IVerificationChannel {
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
public static getTransactionId(event: MatrixEvent): string {
public static getTransactionId(event: MatrixEvent): string | undefined {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
return event.getId();
} else {
const relation = event.getRelation();
if (relation && relation.rel_type === M_REFERENCE) {
if (relation?.rel_type === M_REFERENCE) {
return relation.event_id;
}
}
@@ -184,10 +184,10 @@ export class InRoomChannel implements IVerificationChannel {
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
public handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
if (request.hasEventId(event.getId()!)) {
return;
}
const type = InRoomChannel.getEventType(event);
@@ -198,7 +198,7 @@ export class InRoomChannel implements IVerificationChannel {
return;
}
// set userId if not set already
if (this.userId === null) {
if (!this.userId) {
const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
if (userId) {
this.userId = userId;
@@ -207,14 +207,13 @@ export class InRoomChannel implements IVerificationChannel {
// ignore events not sent by us or the other party
const ownUserId = this.client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (this.userId) {
if (sender !== ownUserId && sender !== this.userId) {
logger.log(`InRoomChannel: ignoring verification event from ` +
`non-participating sender ${sender}`);
logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`);
return;
}
}
if (this.requestEventId === null) {
if (!this.requestEventId) {
this.requestEventId = InRoomChannel.getTransactionId(event);
}
@@ -236,7 +235,7 @@ export class InRoomChannel implements IVerificationChannel {
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation();
content[M_RELATES_TO] = event.getRelation()!;
return content;
}
@@ -307,17 +306,17 @@ export class InRoomChannel implements IVerificationChannel {
export class InRoomRequests implements IRequestsMap {
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
public getRequest(event: MatrixEvent): VerificationRequest {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
public getRequest(event: MatrixEvent): VerificationRequest | undefined {
const roomId = event.getRoomId()!;
const txnId = InRoomChannel.getTransactionId(event)!;
return this.getRequestByTxnId(roomId, txnId);
}
public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
return this.getRequestByTxnId(channel.roomId, channel.transactionId);
public getRequestByChannel(channel: InRoomChannel): VerificationRequest | undefined {
return this.getRequestByTxnId(channel.roomId, channel.transactionId!);
}
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest | undefined {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
@@ -325,11 +324,11 @@ export class InRoomRequests implements IRequestsMap {
}
public setRequest(event: MatrixEvent, request: VerificationRequest): void {
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
this.doSetRequest(event.getRoomId()!, InRoomChannel.getTransactionId(event)!, request);
}
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
this.doSetRequest(channel.roomId, channel.transactionId, request);
this.doSetRequest(channel.roomId!, channel.transactionId!, request);
}
private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
@@ -342,17 +341,17 @@ export class InRoomRequests implements IRequestsMap {
}
public removeRequest(event: MatrixEvent): void {
const roomId = event.getRoomId();
const roomId = event.getRoomId()!;
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
requestsByTxnId.delete(InRoomChannel.getTransactionId(event)!);
if (requestsByTxnId.size === 0) {
this.requestsByRoomId.delete(roomId);
}
}
}
public findRequestInProgress(roomId: string): VerificationRequest {
public findRequestInProgress(roomId: string): VerificationRequest | undefined {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
@@ -46,8 +46,8 @@ export class ToDeviceChannel implements IVerificationChannel {
private readonly client: MatrixClient,
public readonly userId: string,
private readonly devices: string[],
public transactionId: string = null,
public deviceId: string = null,
public transactionId?: string,
public deviceId?: string,
) {}
public isToDevices(devices: string[]): boolean {
@@ -173,13 +173,11 @@ export class ToDeviceChannel implements IVerificationChannel {
return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const wasStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
const isStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const isStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
@@ -256,16 +254,16 @@ export class ToDeviceChannel implements IVerificationChannel {
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
result = await this.sendToDevices(type, content, this.devices);
} else {
result = await this.sendToDevices(type, content, [this.deviceId]);
result = await this.sendToDevices(type, content, [this.deviceId!]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this.client.getUserId(),
sender: this.client.getUserId()!,
content,
type,
});
await this.request.handleEvent(
await this.request!.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
@@ -298,18 +296,18 @@ export class ToDeviceChannel implements IVerificationChannel {
export class ToDeviceRequests implements IRequestsMap {
private requestsByUserId = new Map<string, Map<string, Request>>();
public getRequest(event: MatrixEvent): Request {
public getRequest(event: MatrixEvent): Request | undefined {
return this.getRequestBySenderAndTxnId(
event.getSender(),
event.getSender()!,
ToDeviceChannel.getTransactionId(event),
);
}
public getRequestByChannel(channel: ToDeviceChannel): Request {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
public getRequestByChannel(channel: ToDeviceChannel): Request | undefined {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId!);
}
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request | undefined {
const requestsByTxnId = this.requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
@@ -317,11 +315,11 @@ export class ToDeviceRequests implements IRequestsMap {
}
public setRequest(event: MatrixEvent, request: Request): void {
this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
this.setRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event), request);
}
public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId!, request);
}
public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
@@ -334,7 +332,7 @@ export class ToDeviceRequests implements IRequestsMap {
}
public removeRequest(event: MatrixEvent): void {
const userId = event.getSender();
const userId = event.getSender()!;
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
@@ -344,7 +342,7 @@ export class ToDeviceRequests implements IRequestsMap {
}
}
public findRequestInProgress(userId: string, devices: string[]): Request {
public findRequestInProgress(userId: string, devices: string[]): Request | undefined {
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
@@ -25,6 +25,7 @@ import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { EventType } from '../../../@types/event';
import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index";
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
@@ -95,25 +96,25 @@ export class VerificationRequest<
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
private timeoutTimer: ReturnType<typeof setTimeout> = null;
private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
private _accepting = false;
private _declining = false;
private verifierHasFinished = false;
private _cancelled = false;
private _chosenMethod: VerificationMethod = null;
private _chosenMethod: VerificationMethod | null = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
private _qrCodeData: QRCodeData = null;
private _qrCodeData: QRCodeData | null = null;
// The timestamp when we received the request event from the other side
private requestReceivedAt: number = null;
private requestReceivedAt: number | null = null;
private commonMethods: VerificationMethod[] = [];
private _phase: Phase;
public _cancellingUserId: string; // Used in tests only
private _verifier: VerificationBase<any, any>;
private _phase!: Phase;
public _cancellingUserId?: string; // Used in tests only
private _verifier?: VerificationBase<any, any>;
constructor(
public readonly channel: C,
@@ -203,7 +204,7 @@ export class VerificationRequest<
}
/** the method picked in the .start event */
public get chosenMethod(): VerificationMethod {
public get chosenMethod(): VerificationMethod | null {
return this._chosenMethod;
}
@@ -235,7 +236,7 @@ export class VerificationRequest<
* The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found.
*/
public get requestEvent(): MatrixEvent {
public get requestEvent(): MatrixEvent | undefined {
return this.getEventByEither(REQUEST_TYPE);
}
@@ -245,7 +246,7 @@ export class VerificationRequest<
}
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
public get verifier(): VerificationBase<any, any> {
public get verifier(): VerificationBase<any, any> | undefined {
return this._verifier;
}
@@ -269,7 +270,7 @@ export class VerificationRequest<
}
/** Only set after a .ready if the other party can scan a QR code */
public get qrCodeData(): QRCodeData {
public get qrCodeData(): QRCodeData | null {
return this._qrCodeData;
}
@@ -339,7 +340,7 @@ export class VerificationRequest<
/** The id of the user that initiated the request */
public get requestingUserId(): string {
if (this.initiatedByMe) {
return this.client.getUserId();
return this.client.getUserId()!;
} else {
return this.otherUserId;
}
@@ -350,13 +351,13 @@ export class VerificationRequest<
if (this.initiatedByMe) {
return this.otherUserId;
} else {
return this.client.getUserId();
return this.client.getUserId()!;
}
}
/** The user id of the other party in this request */
public get otherUserId(): string {
return this.channel.userId;
return this.channel.userId!;
}
public get isSelfVerification(): boolean {
@@ -367,11 +368,11 @@ export class VerificationRequest<
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
public get cancellingUserId(): string {
public get cancellingUserId(): string | undefined {
const myCancel = this.eventsByUs.get(CANCEL_TYPE);
const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
if (myCancel && (!theirCancel || myCancel.getId()! < theirCancel.getId()!)) {
return myCancel.getSender();
}
if (theirCancel) {
@@ -404,8 +405,8 @@ export class VerificationRequest<
this.eventsByThem.get(REQUEST_TYPE) ||
this.eventsByThem.get(READY_TYPE) ||
this.eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
const theirFirstContent = theirFirstEvent?.getContent();
const fromDevice = theirFirstContent?.from_device;
return {
userId: this.otherUserId,
deviceId: fromDevice,
@@ -421,7 +422,7 @@ export class VerificationRequest<
*/
public beginKeyVerification(
method: VerificationMethod,
targetDevice: ITargetDevice = null,
targetDevice: ITargetDevice | null = null,
): VerificationBase<any, any> {
// need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) {
@@ -442,7 +443,7 @@ export class VerificationRequest<
this._chosenMethod = method;
}
}
return this._verifier;
return this._verifier!;
}
/**
@@ -469,7 +470,7 @@ export class VerificationRequest<
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)());
} else {
this._cancellingUserId = this.client.getUserId();
this._cancellingUserId = this.client.getUserId()!;
await this.channel.send(CANCEL_TYPE, { code, reason });
}
}
@@ -524,11 +525,11 @@ export class VerificationRequest<
}
}
private getEventByEither(type: string): MatrixEvent {
private getEventByEither(type: string): MatrixEvent | undefined {
return this.eventsByThem.get(type) || this.eventsByUs.get(type);
}
private getEventBy(type: string, byThem = false): MatrixEvent {
private getEventBy(type: string, byThem = false): MatrixEvent | undefined {
if (byThem) {
return this.eventsByThem.get(type);
} else {
@@ -547,20 +548,20 @@ export class VerificationRequest<
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
}
const readyEvent =
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({ phase: PHASE_READY, event: readyEvent });
}
let startEvent;
let startEvent: MatrixEvent | undefined;
if (readyEvent || !requestEvent) {
const theirStartEvent = this.eventsByThem.get(START_TYPE);
const ourStartEvent = this.eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
theirStartEvent : ourStartEvent;
startEvent = theirStartEvent.getSender()! < ourStartEvent.getSender()!
? theirStartEvent
: ourStartEvent;
} else {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
}
@@ -568,7 +569,9 @@ export class VerificationRequest<
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
}
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
const fromRequestPhase = (
phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender()
);
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({ phase: PHASE_STARTED, event: startEvent });
@@ -594,7 +597,7 @@ export class VerificationRequest<
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent<{
const content = event!.getContent<{
methods: string[];
}>();
this.commonMethods =
@@ -619,7 +622,7 @@ export class VerificationRequest<
}
// create verifier
if (phase === PHASE_STARTED) {
const { method } = event.getContent();
const { method } = event!.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this.createVerifier(method, event);
if (!this._verifier) {
@@ -650,7 +653,7 @@ export class VerificationRequest<
if (newEvent.getType() !== START_TYPE) {
return false;
}
const oldEvent = this._verifier.startEvent;
const oldEvent = this._verifier!.startEvent;
let oldRaceIdentifier;
if (this.isSelfVerification) {
@@ -889,9 +892,9 @@ export class VerificationRequest<
private createVerifier(
method: VerificationMethod,
startEvent: MatrixEvent = null,
targetDevice: ITargetDevice = null,
): VerificationBase<any, any> {
startEvent: MatrixEvent | null = null,
targetDevice: ITargetDevice | null = null,
): VerificationBase<any, any> | undefined {
if (!targetDevice) {
targetDevice = this.targetDevice;
}
@@ -902,19 +905,19 @@ export class VerificationRequest<
logger.warn("could not find verifier constructor for method", method);
return;
}
return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
return new VerifierCtor(this.channel, this.client, userId!, deviceId!, startEvent, this);
}
private wasSentByOwnUser(event: MatrixEvent): boolean {
return event.getSender() === this.client.getUserId();
private wasSentByOwnUser(event?: MatrixEvent): boolean {
return event?.getSender() === this.client.getUserId();
}
// only for .request, .ready or .start
private wasSentByOwnDevice(event: MatrixEvent): boolean {
private wasSentByOwnDevice(event?: MatrixEvent): boolean {
if (!this.wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();
const content = event!.getContent();
if (!content || content.from_device !== this.client.getDeviceId()) {
return false;
}
@@ -931,7 +934,7 @@ export class VerificationRequest<
}
public onVerifierFinished(): void {
this.channel.send("m.key.verification.done", {});
this.channel.send(EventType.KeyVerificationDone, {});
this.verifierHasFinished = true;
// move to .done phase
const newTransitions = this.applyPhaseTransitions();
@@ -940,7 +943,7 @@ export class VerificationRequest<
}
}
public getEventFromOtherParty(type: string): MatrixEvent {
public getEventFromOtherParty(type: string): MatrixEvent | undefined {
return this.eventsByThem.get(type);
}
}
-52
View File
@@ -1,52 +0,0 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
export function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
export function InvalidCryptoStoreError(reason) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.name = 'InvalidCryptoStoreError';
return instance;
}
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
export class KeySignatureUploadError extends Error {
constructor(message, value) {
super(message);
this.value = value;
}
}

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