Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47b729f085 | |||
| 05d980608a | |||
| 99af67a963 | |||
| b77f5a5598 | |||
| 76377c7cc4 | |||
| 52830a2a50 | |||
| 81c3668cb6 | |||
| 8f40dc6304 | |||
| fcdd8c93f4 | |||
| 7d7803380c | |||
| 9fa6616052 | |||
| 1f3ae4bde2 | |||
| 545a74364d | |||
| 646b3a69fe | |||
| db33f396b8 | |||
| 6c475d9b54 | |||
| 068fbb7660 | |||
| b44787192d | |||
| 6f2390a765 | |||
| dddc0aeccb | |||
| 9f6b42d3ae | |||
| 2e56c34df0 | |||
| 9f2f08dfd3 | |||
| 4b3e6939d6 | |||
| f2ae3bc8ef | |||
| 11cc30f345 | |||
| 24a9562b07 | |||
| 8f10c0d921 | |||
| 9bdeea0a8d | |||
| ade2e81d3d | |||
| 2fe434f3ae | |||
| 3a6561af36 | |||
| 403286cb81 | |||
| 219eab9139 | |||
| a12e6185f9 | |||
| d9eac57e9c | |||
| 9a9009d838 | |||
| 6f729ad7fd | |||
| cd33bafa04 | |||
| 867a0ca7ee | |||
| fdbbd9bca4 | |||
| bf1137fc58 | |||
| 508bb5841c | |||
| 35227e3a75 | |||
| 620a8d9c7f | |||
| 17e16b9b1a | |||
| 671dedca1c | |||
| 2464a691ef | |||
| d548b04d06 | |||
| 7ffdf17213 | |||
| 1c3dd0e51e | |||
| 0231d40277 | |||
| b1e9f39d65 |
+7
-7
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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", () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()!,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
let deviceLists: DeviceList[] = [];
|
||||
|
||||
beforeEach(function() {
|
||||
deviceLists = [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(",")}]`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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][] } = {};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user