1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event

Conflicts:
	spec/unit/matrix-client.spec.ts
	src/client.ts
This commit is contained in:
Eric Eastwood
2022-12-08 18:29:14 -06:00
163 changed files with 7453 additions and 6686 deletions

View File

@ -2,6 +2,7 @@ module.exports = {
plugins: [
"matrix-org",
"import",
"jsdoc",
],
extends: [
"plugin:matrix-org/babel",
@ -45,7 +46,7 @@ module.exports = {
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
message: "Please use TypedEventEmitter instead",
}],
"import/no-restricted-paths": ["error", {
@ -61,6 +62,9 @@ module.exports = {
files: [
"**/*.ts",
],
plugins: [
"eslint-plugin-tsdoc",
],
extends: [
"plugin:matrix-org/typescript",
],
@ -84,6 +88,23 @@ module.exports = {
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
},
}, {
// We don't need amazing docs in our spec files
files: [
"src/**/*.ts",
],
rules: {
"tsdoc/syntax": "error",
// We use some select jsdoc rules as the tsdoc linter has only one rule
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/check-values": "error",
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
},
}, {
files: [

10
.github/CODEOWNERS vendored
View File

@ -1,4 +1,6 @@
* @matrix-org/element-web
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
* @matrix-org/element-web
/.github/workflows/** @matrix-org/element-web-app-team
/package.json @matrix-org/element-web-app-team
/yarn.lock @matrix-org/element-web-app-team
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers

View File

@ -42,7 +42,7 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v1
uses: tspascoal/get-user-teams-membership@v2
id: teams
with:
username: ${{ github.event.pull_request.user.login }}

View File

@ -36,7 +36,7 @@ jobs:
tag="${{ github.ref_name }}"
VERSION="${tag#v}"
[ ! -e "$VERSION" ] || rm -r $VERSION
cp -r $RUNNER_TEMP/docs/ $VERSION
cp -r $RUNNER_TEMP/_docs/ $VERSION
# Add the new directory to the index if it isn't there already
if ! grep -q ">Version $VERSION</a>" index.html; then

View File

@ -72,38 +72,3 @@ jobs:
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--noImplicitAny'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}

View File

@ -1,3 +1,28 @@
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
==================================================================================================
## 🚨 BREAKING CHANGES
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
## 🦖 Deprecations
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
## ✨ Features
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
## 🐛 Bug Fixes
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
==================================================================================================

View File

@ -6,7 +6,7 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
Matrix Javascript SDK
Matrix JavaScript SDK
=====================
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
@ -370,11 +370,14 @@ To build a browser version from scratch when developing::
$ yarn build
```
To run tests (Jasmine)::
To run tests (Jest):
```
$ yarn test
```
> **Note**
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting:
```
$ yarn lint

View File

@ -1,9 +1,9 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ npm run build
$ yarn build
$ cd examples/browser
$ python -m SimpleHTTPServer 8003
$ python -m http.server 8003
```
Then visit ``http://localhost:8003``.

View File

@ -1,11 +1,7 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));
return;
}
var client = matrixcs.createClient({baseUrl: "https://matrix.org"});
client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!");
var result = document.getElementById("result");

View File

@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "21.2.0",
"version": "22.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@ -14,7 +14,7 @@
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
@ -33,9 +33,9 @@
"matrix-org"
],
"main": "./src/index.ts",
"browser": "./lib/browser-index.js",
"browser": "./lib/browser-index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
@ -54,7 +54,6 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/sdp-transform": "^2.4.5",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@ -64,7 +63,8 @@
"p-retry": "4",
"qs": "^6.9.6",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6"
"unhomoglyph": "^1.0.6",
"uuid": "7"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
@ -80,14 +80,16 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "7",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
@ -99,7 +101,9 @@
"eslint-config-google": "^0.14.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.6.4",
"eslint-plugin-matrix-org": "^0.8.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",

View File

@ -105,7 +105,7 @@ export class TestClient {
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
* @returns Promise which resolves once the mock http backend has finished all pending flushes
*/
public async stop(): Promise<void> {
this.client.stopClient();
@ -135,7 +135,7 @@ export class TestClient {
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
* @returns Promise for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys!).length != 0) {
@ -177,13 +177,13 @@ export class TestClient {
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
* @param response - response to the query.
*/
public expectKeyQuery(response: IDownloadKeyResult) {
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! as Record<string, any>)[userId]).toEqual([]);
});
return response;
});
@ -202,7 +202,7 @@ export class TestClient {
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
@ -212,7 +212,7 @@ export class TestClient {
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;

View File

@ -15,19 +15,7 @@ limitations under the License.
*/
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { MatrixClient, ClientEvent } from "../../src";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
import type { default as BrowserMatrix } from "../../src/browser-index";
// stub for browser-matrix browserify tests
// @ts-ignore
@ -43,4 +31,4 @@ afterAll(() => {
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
};
} as typeof BrowserMatrix;

View File

@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import "./setupTests";// uses browser-matrix instead of the src
import "./setupTests"; // uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
@ -65,15 +65,16 @@ describe("Browserify Test", function() {
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[ROOM_ID] = {
timeline: {
events: [
event,
],
limited: false,
join: {
[ROOM_ID]: {
timeline: {
events: [
event,
],
limited: false,
},
},
},
},
};

View File

@ -26,11 +26,9 @@ const ROOM_ID = "!room:id";
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
* @returns sync response
*/
function getSyncResponse(roomMembers) {
function getSyncResponse(roomMembers: string[]) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
@ -43,12 +41,10 @@ function getSyncResponse(roomMembers) {
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
sender: m,
}),
),
roomMembers.map((m) => testUtils.mkMembership({
mship: 'join',
sender: m,
})),
);
const syncResponse = {
@ -73,8 +69,8 @@ describe("DeviceList management:", function() {
return;
}
let sessionStoreBackend;
let aliceTestClient;
let aliceTestClient: TestClient;
let sessionStoreBackend: Storage;
async function createTestClient() {
const testClient = new TestClient(
@ -97,7 +93,10 @@ describe("DeviceList management:", function() {
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} },
failures: {},
});
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
@ -138,7 +137,10 @@ describe("DeviceList management:", function() {
it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
aliceTestClient.expectKeyQuery({
device_keys: { '@alice:localhost': {} },
failures: {},
});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
@ -164,11 +166,12 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
expect(data!.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
@ -201,15 +204,16 @@ describe("DeviceList management:", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
const chrisStat = data!.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
@ -234,12 +238,13 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
const chrisStat = data!.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
@ -255,15 +260,16 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
const chrisStat = data!.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
expect(data!.syncToken).toEqual(3);
});
});
});
@ -285,10 +291,11 @@ describe("DeviceList management:", function() {
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
@ -322,10 +329,11 @@ describe("DeviceList management:", function() {
);
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
@ -359,15 +367,14 @@ describe("DeviceList management:", function() {
);
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0,
);
expect(bobStat).toEqual(0);
});
});
@ -388,9 +395,7 @@ describe("DeviceList management:", function() {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0,
);
expect(bobStat).toEqual(0);
});
} finally {
anotherTestClient.stop();

View File

@ -28,12 +28,14 @@ limitations under the License.
// load olm before the sdk if possible
import '../olm-loader';
import type { Session } from "@matrix-org/olm";
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@ -47,36 +49,29 @@ const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[];
let bobMessages: IContent[];
// IMessage isn't exported by src/crypto/algorithms/olm.ts
interface OlmPayload {
type: number;
body: string;
}
type OlmPayload = ReturnType<Session["encrypt"]>;
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
await bobTestClient.httpBackend.flushAllExpected();
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}
/**
* Set an expectation that querier will query uploader's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
* @returns resolves once the http request has completed.
*/
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
// can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) {
.respond(200, function(_path, content: IQueryKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {};
const result: Record<string, Record<string, IDeviceKeys>> = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
@ -88,13 +83,13 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
* @returns resolves once the http request has completed.
*/
async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(_path, content: IUploadKeysRequest) {
).respond(200, function(_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
@ -105,7 +100,7 @@ async function expectAliClaimKeys(): Promise<void> {
}
}
}
const result = {};
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
@ -156,7 +151,7 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client)
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -173,7 +168,7 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -188,7 +183,7 @@ async function aliSendsMessage(): Promise<OlmPayload> {
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
* @returns which resolves to the ciphertext for Ali's device.
*/
async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -203,7 +198,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
@ -217,7 +212,7 @@ async function expectAliSendMessageRequest(): Promise<OlmPayload> {
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
@ -276,22 +271,21 @@ async function recvMessage(
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
},
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
@ -327,32 +321,32 @@ async function recvMessage(
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
* @returns which resolves when the sync has been flushed.
*/
function firstSync(testClient: TestClient): Promise<void> {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
join: {
[roomId]: {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
},
},
},
};
@ -385,6 +379,14 @@ describe("MatrixClient crypto", () => {
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("handles failures to upload device keys", async () => {
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
// upload.
bobTestClient.httpBackend.when("POST", "/keys/upload")
.fail(0, new Error("bleh"));
await bobTestClient.httpBackend.flushAllExpected();
});
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
@ -424,7 +426,7 @@ describe("MatrixClient crypto", () => {
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
@ -460,7 +462,7 @@ describe("MatrixClient crypto", () => {
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
@ -515,22 +517,21 @@ describe("MatrixClient crypto", () => {
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
},
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
@ -607,20 +608,21 @@ describe("MatrixClient crypto", () => {
const syncData = {
next_batch: '2',
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
join: {
[roomId]: {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
},
}),
],
},
}),
],
},
},
},
};
@ -679,4 +681,45 @@ describe("MatrixClient crypto", () => {
});
await httpBackend.flushAllExpected();
});
it("Checks for outgoing room key requests for a given event's session", async () => {
const eventA0 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventA1 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventB = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
session_id: "othersessionid",
sender_key: "senderkey",
},
});
const nonEncryptedEvent = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {},
});
aliTestClient.client.crypto?.onSyncCompleted({});
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
});
});

View File

@ -27,11 +27,13 @@ import {
MatrixEvent,
PendingEventOrdering,
Room,
RoomEvent,
} from "../../src/matrix";
import { logger } from "../../src/logger";
import { encodeUri } from "../../src/utils";
import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";
const userId = "@alice:localhost";
const userName = "Alice";
@ -534,20 +536,20 @@ describe("MatrixClient event timelines", function() {
};
});
let tl0;
let tl3;
let tl0: EventTimeline;
let tl3: EventTimeline;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1);
tl0 = tl;
tl0 = tl!;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id!);
}).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id!);
}).then(function(tl) {
expect(tl!.getEvents().length).toEqual(1);
tl3 = tl;
tl3 = tl!;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id!);
}).then(function(tl) {
// we expect it to get merged in with event 2
@ -611,7 +613,12 @@ describe("MatrixClient event timelines", function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
.respond(200, function() {
@ -650,7 +657,6 @@ describe("MatrixClient event timelines", function() {
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
};
@ -660,11 +666,17 @@ describe("MatrixClient event timelines", function() {
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const timeline = await timelinePromise;
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const [timeline] = await Promise.all([timelinePromise, httpBackend.flushAllExpected()]);
const eventIds = timeline!.getEvents().map(it => it.getId());
expect(eventIds).toContain(THREAD_ROOT.event_id);
expect(eventIds).toContain(THREAD_REPLY.event_id);
});
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
@ -941,11 +953,11 @@ describe("MatrixClient event timelines", function() {
};
});
let tl;
let tl: EventTimeline;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl0) {
tl = tl0;
tl = tl0!;
return client.paginateEventTimeline(tl, { backwards: true });
}).then(function(success) {
expect(success).toBeTruthy();
@ -1031,11 +1043,11 @@ describe("MatrixClient event timelines", function() {
};
});
let tl;
let tl: EventTimeline;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id!,
).then(function(tl0) {
tl = tl0;
tl = tl0!;
return client.paginateEventTimeline(
tl, { backwards: false, limit: 20 });
}).then(function(success) {
@ -1083,10 +1095,27 @@ describe("MatrixClient event timelines", function() {
return request;
}
function respondToContext(): ExpectedHttpRequest {
function respondToThread(
root: Partial<IEvent>,
replies: Partial<IEvent>[],
): ExpectedHttpRequest {
const request = httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(root.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1");
request.respond(200, function() {
return {
original_event: root,
chunk: [replies],
// no next batch as this is the oldest end of the timeline
};
});
return request;
}
function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
$roomId: roomId,
$eventId: THREAD_ROOT.event_id!,
$eventId: event.event_id!,
}));
request.respond(200, {
end: `${Direction.Forward}${RANDOM_TOKEN}1`,
@ -1094,10 +1123,18 @@ describe("MatrixClient event timelines", function() {
state: [],
events_before: [],
events_after: [],
event: THREAD_ROOT,
event: event,
});
return request;
}
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: event.event_id!,
}));
request.respond(200, event);
return request;
}
function respondToMessagesRequest(): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
$roomId: roomId,
@ -1183,6 +1220,127 @@ describe("MatrixClient event timelines", function() {
expect(myThreads.getPendingEvents()).toHaveLength(0);
expect(room.getPendingEvents()).toHaveLength(1);
});
it("should handle thread updates by reordering the thread list", async () => {
// Test data for a second thread
const THREAD2_ROOT = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread root",
"msgtype": "m.text",
},
unsigned: {
"m.relations": {
"io.element.thread": {
//"latest_event": undefined,
"count": 1,
"current_user_participated": true,
},
},
},
event: false,
});
const THREAD2_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: false,
});
// @ts-ignore we know this is a defined path for THREAD ROOT
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;
// Test data for a second reply to the first thread
const THREAD_REPLY2 = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: false,
});
// Test data for the first thread, with the second reply
const THREAD_ROOT_UPDATED = {
...THREAD_ROOT,
unsigned: {
...THREAD_ROOT.unsigned,
"m.relations": {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 2,
latest_event: THREAD_REPLY2,
},
},
},
};
// Response with test data for the thread list request
const threadsResponse = {
chunk: [THREAD2_ROOT, THREAD_ROOT],
state: [],
next_batch: RANDOM_TOKEN as string | null,
};
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
// Setup room threads
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
respondToThreads(threadsResponse);
respondToThreads(threadsResponse);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD2_ROOT);
respondToEvent(THREAD2_ROOT);
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
await flushHttp(room.fetchRoomThreads());
const [allThreads] = timelineSets!;
const timeline = allThreads.getLiveTimeline()!;
// Test threads are in chronological order
expect(timeline.getEvents().map(it => it.event.event_id))
.toEqual([THREAD_ROOT.event_id, THREAD2_ROOT.event_id]);
// Test adding a second event to the first thread
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const prom = emitPromise(allThreads!, RoomEvent.Timeline);
await thread.addEvent(client.getEventMapper()(THREAD_REPLY2), false);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD_ROOT_UPDATED);
await httpBackend.flushAllExpected();
await prom;
// Test threads are in chronological order
expect(timeline!.getEvents().map(it => it.event.event_id))
.toEqual([THREAD2_ROOT.event_id, THREAD_ROOT.event_id]);
});
});
describe("without server compatibility", function() {
@ -1411,16 +1569,17 @@ describe("MatrixClient event timelines", function() {
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
join: {
[roomId]: {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
@ -1437,18 +1596,19 @@ describe("MatrixClient event timelines", function() {
const sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
join: {
[roomId]: {
timeline: {
events: [
utils.mkMessage({
user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
@ -1504,7 +1664,22 @@ describe("MatrixClient event timelines", function() {
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" +
encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" +
encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" +
encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
.respond(200, function() {
@ -1513,7 +1688,7 @@ describe("MatrixClient event timelines", function() {
chunk: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
.respond(200, function() {

View File

@ -511,7 +511,12 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
return client!.initCrypto();
// running initCrypto should trigger a key upload
httpBackend!.when("POST", "/keys/upload").respond(200, {});
return Promise.all([
client!.initCrypto(),
httpBackend!.flush("/keys/upload", 1),
]);
});
afterEach(() => {
@ -618,13 +623,13 @@ describe("MatrixClient", function() {
});
describe("partitionThreadedEvents", function() {
let room;
let room: Room;
beforeEach(() => {
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId);
});
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const events: MatrixEvent[] = [];
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
@ -1337,27 +1342,45 @@ describe("MatrixClient", function() {
});
});
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,
};
describe("setPowerLevel", () => {
it.each([
{
userId: "alice@localhost",
expectation: {
"alice@localhost": 100,
},
},
{
userId: ["alice@localhost", "bob@localhost"],
expectation: {
"alice@localhost": 100,
"bob@localhost": 100,
},
},
])("should modify power levels of $userId correctly", async ({ userId, expectation }) => {
const event = {
getType: () => "m.room.power_levels",
getContent: () => ({
users: {
"alice@localhost": 50,
},
}),
} as MatrixEvent;
httpBackend!.when("POST", "/account/register").check(req => {
expect(req.data).toStrictEqual(token);
}).respond(200, {
access_token: "at",
token: "tt",
});
httpBackend!.when("PUT", "/state/m.room.power_levels").check(req => {
expect(req.data.users).toStrictEqual(expectation);
}).respond(200, {});
const prom = client!.registerWithIdentityServer(token);
const prom = client!.setPowerLevel("!room_id:server", userId, 100, event);
await httpBackend!.flushAllExpected();
const resp = await prom;
expect(resp.access_token).toBe("at");
expect(resp.token).toBe("tt");
await prom;
});
});
describe("uploadKeys", () => {
// uploadKeys() is a no-op nowadays, so there's not much to test here.
it("should complete successfully", async () => {
await client!.uploadKeys();
});
});
});
@ -1634,7 +1657,7 @@ const buildEventCreate = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj: object, expected: any): void {
function assertObjectContains(obj: Record<string, any>, expected: any): void {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);

View File

@ -1,7 +1,7 @@
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { MatrixClient } from "../../src/matrix";
import { ClientEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
@ -65,7 +65,7 @@ describe("MatrixClient opts", function() {
});
describe("without opts.store", function() {
let client;
let client: MatrixClient;
beforeEach(function() {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
@ -98,7 +98,7 @@ describe("MatrixClient opts", function() {
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create",
];
client.on("event", function(event) {
client.on(ClientEvent.Event, function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1,
);
@ -125,7 +125,7 @@ describe("MatrixClient opts", function() {
});
describe("without opts.scheduler", function() {
let client;
let client: MatrixClient;
beforeEach(function() {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,

View File

@ -55,7 +55,10 @@ describe("MatrixClient relations", () => {
const response = client!.relations(roomId, '$event-0', null, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend!
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
@ -67,7 +70,10 @@ describe("MatrixClient relations", () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend!
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
@ -78,10 +84,13 @@ describe("MatrixClient relations", () => {
it("should read related events with relation type and event type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
httpBackend!
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
"/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
@ -98,10 +107,13 @@ describe("MatrixClient relations", () => {
to: 'TO',
});
httpBackend!
.when("GET", "/rooms/!room%3Ahere/event/%24event-0")
.respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
"/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });

View File

@ -18,7 +18,16 @@ import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import {
MatrixError,
ClientEvent,
IEvent,
MatrixClient,
RoomEvent,
ISyncResponse,
IMinimalEvent,
IRoomEvent, Room,
} from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
@ -39,7 +48,7 @@ describe("MatrixClient room timelines", function() {
name: "Old room name",
},
});
let NEXT_SYNC_DATA;
let NEXT_SYNC_DATA: Partial<ISyncResponse>;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
@ -88,7 +97,7 @@ describe("MatrixClient room timelines", function() {
},
},
leave: {},
},
} as unknown as ISyncResponse["rooms"],
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
@ -96,11 +105,11 @@ describe("MatrixClient room timelines", function() {
}
if (e.state_key) {
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
NEXT_SYNC_DATA.rooms!.join[roomId].ephemeral.events.push(e as unknown as IMinimalEvent);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
}
});
}
@ -237,7 +246,7 @@ describe("MatrixClient room timelines", function() {
});
describe("paginated events", function() {
let sbEvents;
let sbEvents: Partial<IEvent>[];
const sbEndTok = "pagin_end";
beforeEach(function() {
@ -559,7 +568,7 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend!.flush("/versions", 1),
@ -593,7 +602,7 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend!.flush("/sync", 1),
@ -638,7 +647,7 @@ describe("MatrixClient room timelines", function() {
end: "end_token",
};
let room;
let room: Room;
beforeEach(async () => {
setNextSyncData(initialSyncEventData);

View File

@ -33,8 +33,9 @@ import {
IJoinedRoom,
IStateEvent,
IMinimalEvent,
NotificationCountType,
NotificationCountType, IEphemeral, Room,
} from "../../src";
import { ReceiptType } from '../../src/@types/read_receipts';
import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync';
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
@ -524,105 +525,101 @@ describe("MatrixClient syncing", () => {
const syncData = {
rooms: {
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
},
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
},
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
const nextSyncData = {
rooms: {
join: {
[roomOne]: {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
},
},
},
};
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
@ -635,9 +632,7 @@ describe("MatrixClient syncing", () => {
]).then(() => {
const room = client!.getRoom(roomOne)!;
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
expect(room.name).toEqual(nextSyncData.rooms.join[roomOne].state.events[0].content?.name);
});
});
@ -742,46 +737,48 @@ describe("MatrixClient syncing", () => {
const normalFirstSync = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
normalFirstSync.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
join: {
[roomOne]: {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
},
},
},
};
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should normally trigger
// `timelineNeedsRefresh=true` but this marker isn't
// being sent by the room creator so it has no
// special meaning in existing room versions.
utils.mkEvent({
type: UNSTABLE_MSC2716_MARKER.name,
room: roomOne,
// The important part we're testing is here!
// `userC` is not the room creator.
user: userC,
skey: "",
content: {
"m.insertion_id": "$abc",
join: {
[roomOne]: {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should normally trigger
// `timelineNeedsRefresh=true` but this marker isn't
// being sent by the room creator so it has no
// special meaning in existing room versions.
utils.mkEvent({
type: UNSTABLE_MSC2716_MARKER.name,
room: roomOne,
// The important part we're testing is here!
// `userC` is not the room creator.
user: userC,
skey: "",
content: {
"m.insertion_id": "$abc",
},
}),
],
prev_batch: "pagTok",
},
}),
],
prev_batch: "pagTok",
},
},
},
};
@ -831,16 +828,17 @@ describe("MatrixClient syncing", () => {
const normalFirstSync = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
normalFirstSync.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
join: {
[roomOne]: {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
},
},
},
};
@ -849,16 +847,17 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
join: {
[roomOne]: {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
},
},
},
};
@ -879,16 +878,17 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [markerEventFromRoomCreator],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
join: {
[roomOne]: {
timeline: {
events: [markerEventFromRoomCreator],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
},
},
},
};
@ -909,19 +909,20 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [
roomCreateEvent,
markerEventFromRoomCreator,
],
join: {
[roomOne]: {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [
roomCreateEvent,
markerEventFromRoomCreator,
],
},
},
},
},
};
@ -942,17 +943,18 @@ describe("MatrixClient syncing", () => {
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
prev_batch: "pagTok",
join: {
[roomOne]: {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
prev_batch: "pagTok",
},
},
},
},
};
@ -993,24 +995,25 @@ describe("MatrixClient syncing", () => {
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello again",
}),
],
prev_batch: "pagTok",
},
state: {
events: [
// In subsequent syncs, a marker event in state
// should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello again",
}),
],
prev_batch: "pagTok",
},
state: {
events: [
// In subsequent syncs, a marker event in state
// should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
},
},
},
},
};
@ -1095,19 +1098,20 @@ describe("MatrixClient syncing", () => {
const limitedSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
limitedSyncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
// The important part, make the sync `limited`
limited: true,
prev_batch: "newerTok",
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
// The important part, make the sync `limited`
limited: true,
prev_batch: "newerTok",
},
},
},
},
};
httpBackend!.when("GET", "/sync").respond(200, limitedSyncData);
@ -1167,7 +1171,7 @@ describe("MatrixClient syncing", () => {
const eventsInRoom = syncData.rooms.join[roomOne].timeline.events;
const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` +
`${encodeURIComponent(eventsInRoom[0].event_id)}`;
`${encodeURIComponent(eventsInRoom[0].event_id!)}`;
httpBackend!.when("GET", contextUrl)
.respond(200, () => {
return {
@ -1202,17 +1206,18 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
},
},
},
};
@ -1229,17 +1234,18 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
join: {
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
},
},
},
},
};
@ -1261,18 +1267,19 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
},
},
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
@ -1304,44 +1311,47 @@ describe("MatrixClient syncing", () => {
const syncData = {
rooms: {
join: {
[roomOne]: {
ephemeral: {
events: [
],
} as IEphemeral,
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
} as Partial<IJoinedRoom>,
},
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
beforeEach(() => {
syncData.rooms.join[roomOne].ephemeral = {
@ -1351,16 +1361,15 @@ describe("MatrixClient syncing", () => {
it("should sync receipts from /sync.", () => {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
const receipt: Record<string, any> = {};
receipt[ackEvent.event_id!] = {
"m.read": {},
};
receipt[ackEvent.event_id]["m.read"][userC] = {
receipt[ackEvent.event_id!]["m.read"][userC] = {
ts: 176592842636,
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt",
}];
httpBackend!.when("GET", "/sync").respond(200, syncData);
@ -1390,6 +1399,9 @@ describe("MatrixClient syncing", () => {
rooms: {
join: {
[roomOne]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkMessage({
@ -1425,7 +1437,7 @@ describe("MatrixClient syncing", () => {
},
},
},
};
} as unknown as ISyncResponse;
it("should sync unread notifications.", () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
@ -1448,6 +1460,50 @@ describe("MatrixClient syncing", () => {
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
});
});
it("caches unknown threads receipts and replay them when the thread is created", async () => {
const THREAD_ID = "$unknownthread:localhost";
const receipt = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 666, thread_id: THREAD_ID },
},
},
},
};
syncData.rooms.join[roomOne].ephemeral.events = [receipt];
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const room = client?.getRoom(roomOne);
expect(room).toBeInstanceOf(Room);
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true);
const thread = room!.createThread(THREAD_ID, undefined, [], true);
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false);
const receipt = thread.getReadReceiptForUserId("@alice:localhost");
expect(receipt).toStrictEqual({
"data": {
"thread_id": "$unknownthread:localhost",
"ts": 666,
},
"eventId": "$event1:localhost",
});
});
});
});
describe("of a room", () => {
@ -1509,18 +1565,18 @@ describe("MatrixClient syncing", () => {
const syncData = {
next_batch: "batch_token",
rooms: {
leave: {},
},
};
syncData.rooms.leave[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
leave: {
[roomTwo]: {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
},
},
},
};
@ -1651,8 +1707,8 @@ describe("MatrixClient syncing", () => {
/**
* waits for the MatrixClient to emit one or more 'sync' events.
*
* @param {Number?} numSyncs number of syncs to wait for
* @returns {Promise} promise which resolves after the sync events have happened
* @param numSyncs - number of syncs to wait for
* @returns promise which resolves after the sync events have happened
*/
function awaitSyncEvent(numSyncs?: number) {
return utils.syncPromise(client!, numSyncs);

View File

@ -126,12 +126,13 @@ describe("megolm key backups", function() {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [ENCRYPTED_EVENT],
join: {
[ROOM_ID]: {
timeline: {
events: [ENCRYPTED_EVENT],
},
},
},
},
};

View File

@ -21,16 +21,19 @@ import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
import {
IContent,
IEvent,
IClaimOTKsResult,
IJoinedRoom,
ISyncResponse,
IContent,
IDownloadKeyResult,
IEvent,
IJoinedRoom,
IndexedDBCryptoStore,
ISyncResponse,
IUploadKeysRequest,
MatrixEvent,
MatrixEventEvent,
IndexedDBCryptoStore,
Room,
RoomMember,
RoomStateEvent,
} from "../../src/matrix";
import { IDeviceKeys } from "../../src/crypto/dehydration";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
@ -217,8 +220,8 @@ describe("megolm", () => {
* Get the device keys for testOlmAccount in a format suitable for a
* response to /keys/query
*
* @param {string} userId The user ID to query for
* @returns {IDownloadKeyResult} The fake query response
* @param userId - The user ID to query for
* @returns The fake query response
*/
function getTestKeysQueryResponse(userId: string): IDownloadKeyResult {
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
@ -245,8 +248,8 @@ describe("megolm", () => {
* Get a one-time key for testOlmAccount in a format suitable for a
* response to /keys/claim
* @param {string} userId The user ID to query for
* @returns {IClaimOTKsResult} The fake key claim response
* @param userId - The user ID to query for
* @returns The fake key claim response
*/
function getTestKeysClaimResponse(userId: string): IClaimOTKsResult {
testOlmAccount.generate_one_time_keys(1);
@ -327,7 +330,9 @@ describe("megolm", () => {
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getContent().body).toEqual('42');
});
@ -873,7 +878,12 @@ describe("megolm", () => {
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await room.decryptCriticalEvents();
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(
room.getLiveTimeline().getEvents()[0], { waitOnDecryptionFailure: true },
);
expect(decryptedEvent.getContent().body).toEqual('42');
const exported = await aliceTestClient.client.exportRoomKeys();
@ -1012,7 +1022,9 @@ describe("megolm", () => {
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
expect(decryptedEvent.getContent()).toEqual({});
expect(decryptedEvent.getClearContent()).toBeUndefined();
@ -1364,4 +1376,90 @@ describe("megolm", () => {
await beccaTestClient.stop();
});
it("allows sending an encrypted event as soon as room state arrives", async () => {
/* Empirically, clients expect to be able to send encrypted events as soon as the
* RoomStateEvent.NewMember notification is emitted, so test that works correctly.
*/
const testRoomId = "!testRoom:id";
await aliceTestClient.start();
aliceTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) {
return { device_keys: {} };
});
/* Alice makes the /createRoom call */
aliceTestClient.httpBackend.when("POST", "/createRoom")
.respond(200, { room_id: testRoomId });
await Promise.all([
aliceTestClient.client.createRoom({
initial_state: [{
type: 'm.room.encryption',
state_key: '',
content: { algorithm: 'm.megolm.v1.aes-sha2' },
}],
}),
aliceTestClient.httpBackend.flushAllExpected(),
]);
/* The sync arrives in two parts; first the m.room.create... */
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
rooms: { join: {
[testRoomId]: {
timeline: { events: [
{
type: 'm.room.create',
state_key: '',
event_id: "$create",
},
{
type: 'm.room.member',
state_key: aliceTestClient.getUserId(),
content: { membership: "join" },
event_id: "$alijoin",
},
] },
},
} },
});
await aliceTestClient.flushSync();
// ... and then the e2e event and an invite ...
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
rooms: { join: {
[testRoomId]: {
timeline: { events: [
{
type: 'm.room.encryption',
state_key: '',
content: { algorithm: 'm.megolm.v1.aes-sha2' },
event_id: "$e2e",
},
{
type: 'm.room.member',
state_key: "@other:user",
content: { membership: "invite" },
event_id: "$otherinvite",
},
] },
},
} },
});
// as soon as the roomMember arrives, try to send a message
aliceTestClient.client.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
if (member.userId == "@other:user") {
aliceTestClient.client.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
}
});
// flush the sync and wait for the /send/ request.
aliceTestClient.httpBackend.when("PUT", "/send/m.room.encrypted/")
.respond(200, (_path, _content) => ({ event_id: "asdfgh" }));
await Promise.all([
aliceTestClient.flushSync(),
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
]);
});
});

View File

@ -23,7 +23,7 @@ import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, RoomEvent, Room, IRoomTimelineData,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
@ -119,13 +119,13 @@ describe("SlidingSyncSdk", () => {
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension;
if (calledExtension && calledExtension.name() === name) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
if (calledExtension?.name() === name) {
return calledExtension;
}
}
@ -170,6 +170,7 @@ describe("SlidingSyncSdk", () => {
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const roomH = "!g_num_live:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
@ -275,6 +276,18 @@ describe("SlidingSyncSdk", () => {
invited_count: 2,
initial: true,
},
[roomH]: {
name: "H",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
],
initial: true,
num_live: 1,
},
};
it("can be created with required_state and timeline", () => {
@ -326,6 +339,33 @@ describe("SlidingSyncSdk", () => {
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with live events", () => {
let seenLiveEvent = false;
const listener = (
ev: MatrixEvent,
room?: Room,
toStartOfTimeline?: boolean,
deleted?: boolean,
timelineData?: IRoomTimelineData,
) => {
if (timelineData?.liveEvent) {
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
seenLiveEvent = true;
}
};
client!.on(RoomEvent.Timeline, listener);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomH].name);
expect(gotRoom.getMyMembership()).toEqual("join");
// check the entire timeline is correct
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
expect(seenLiveEvent).toBe(true);
});
it("can be created with invite_state", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE);
@ -541,7 +581,7 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionE2EE", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient({
@ -607,7 +647,7 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionAccountData", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
@ -733,7 +773,7 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionToDevice", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
@ -831,7 +871,7 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionTyping", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
@ -930,7 +970,7 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionReceipts", () => {
let ext: Extension;
let ext: Extension<any, any>;
const generateReceiptResponse = (
userId: string, roomId: string, eventId: string, recType: string, ts: number,

View File

@ -18,7 +18,15 @@ limitations under the License.
import EventEmitter from "events";
import MockHttpBackend from "matrix-mock-request";
import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from "../../src/sliding-sync";
import {
SlidingSync,
SlidingSyncState,
ExtensionState,
SlidingSyncEvent,
Extension,
SlidingSyncEventHandlerMap,
MSC3575RoomData,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src";
@ -94,7 +102,7 @@ describe("SlidingSync", () => {
is_dm: true,
},
};
const ext = {
const ext: Extension<any, any> = {
name: () => "custom_extension",
onRequest: (initial) => { return { initial: initial }; },
onResponse: (res) => { return {}; },
@ -107,7 +115,7 @@ describe("SlidingSync", () => {
slidingSync.start();
// expect everything to be sent
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -390,8 +398,8 @@ describe("SlidingSync", () => {
}],
rooms: rooms,
});
const listenerData = {};
const dataListener = (roomId, roomData) => {
const listenerData: Record<string, MSC3575RoomData> = {};
const dataListener: SlidingSyncEventHandlerMap[SlidingSyncEvent.RoomData] = (roomId, roomData) => {
expect(listenerData[roomId]).toBeFalsy();
listenerData[roomId] = roomData;
};
@ -912,7 +920,7 @@ describe("SlidingSync", () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
// modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -944,7 +952,7 @@ describe("SlidingSync", () => {
ranges: [[0, 20]],
};
const promise = slidingSync.setList(0, newList);
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -966,7 +974,7 @@ describe("SlidingSync", () => {
});
it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -992,7 +1000,7 @@ describe("SlidingSync", () => {
const promise = slidingSync.modifyRoomSubscriptionInfo({
timeline_limit: 99,
});
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -1016,7 +1024,7 @@ describe("SlidingSync", () => {
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds: any[] = [];
const pushTxn = function(req) {
const pushTxn = function(req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data.txn_id);
};
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
@ -1032,7 +1040,7 @@ describe("SlidingSync", () => {
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id;
}).respond(200, () => {
@ -1050,7 +1058,7 @@ describe("SlidingSync", () => {
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds: any[] = [];
const pushTxn = function(req) {
const pushTxn = function(req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data?.txn_id);
};
const A = slidingSync.setListRanges(0, [[20, 40]]);
@ -1087,7 +1095,7 @@ describe("SlidingSync", () => {
promise.finally(() => {
pending = false;
});
let txnId;
let txnId: string | undefined;
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
@ -1275,21 +1283,21 @@ describe("SlidingSync", () => {
// Pre-extensions get called BEFORE processing the sync response
const preExtName = "foobar";
let onPreExtensionRequest;
let onPreExtensionResponse;
let onPreExtensionRequest: Extension<any, any>["onRequest"];
let onPreExtensionResponse: Extension<any, any>["onResponse"];
// Post-extensions get called AFTER processing the sync response
const postExtName = "foobar2";
let onPostExtensionRequest;
let onPostExtensionResponse;
let onPostExtensionRequest: Extension<any, any>["onRequest"];
let onPostExtensionResponse: Extension<any, any>["onResponse"];
const extPre = {
const extPre: Extension<any, any> = {
name: () => preExtName,
onRequest: (initial) => { return onPreExtensionRequest(initial); },
onResponse: (res) => { return onPreExtensionResponse(res); },
when: () => ExtensionState.PreProcess,
};
const extPost = {
const extPost: Extension<any, any> = {
name: () => postExtName,
onRequest: (initial) => { return onPostExtensionRequest(initial); },
onResponse: (res) => { return onPostExtensionResponse(res); },
@ -1421,7 +1429,7 @@ describe("SlidingSync", () => {
});
function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } {
let timeoutId;
let timeoutId: NodeJS.Timeout;
return {
promise: new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
@ -1438,11 +1446,11 @@ function timeout(delayMs: number, reason: string): { promise: Promise<never>, ca
/**
* Listen until a callback returns data.
* @param {EventEmitter} emitter The event emitter
* @param {string} eventName The event to listen for
* @param {function} callback The callback which will be invoked when events fire. Return something truthy from this to resolve the promise.
* @param {number} timeoutMs The number of milliseconds to wait for the callback to return data. Default: 500ms.
* @returns {Promise} A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached,
* @param emitter - The event emitter
* @param eventName - The event to listen for
* @param callback - The callback which will be invoked when events fire. Return something truthy from this to resolve the promise.
* @param timeoutMs - The number of milliseconds to wait for the callback to return data. Default: 500ms.
* @returns A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached,
* the promise is rejected.
*/
function listenUntil<T>(
@ -1454,7 +1462,7 @@ function listenUntil<T>(
const trace = new Error().stack?.split(`\n`)[2];
const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace);
return Promise.race([new Promise<T>((resolve, reject) => {
const wrapper = (...args) => {
const wrapper = (...args: any[]) => {
try {
const data = callback(...args);
if (data) {

View File

@ -13,9 +13,9 @@ import { eventMapperFor } from "../../src/event-mapper";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
* @param client - The client
* @param count - Number of syncs to wait for (default 1)
* @returns Promise which resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
@ -41,9 +41,9 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
* @param constr - The class constructor (used with 'new')
* @param name - The name of the class
* @returns An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
@ -84,15 +84,15 @@ interface IEventOpts {
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
* @param opts - Values for the event.
* @param opts.type - The event.type
* @param opts.room - The event.room_id
* @param opts.sender - The event.sender
* @param opts.skey - Optional. The state key (auto inserts empty string)
* @param opts.content - The event.content
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@ -160,8 +160,8 @@ interface IPresenceOpts {
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the presence.
* @returns The event
*/
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
@ -193,16 +193,16 @@ interface IMembershipOpts {
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* @param opts - Values for the membership.
* @param opts.room - The room ID for the event.
* @param opts.mship - The content.membership for the event.
* @param opts.sender - The sender user ID for the event.
* @param opts.skey - The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
* @param opts.name - The content.displayname for the event.
* @param opts.url - The content.avatar_url for the event.
* @param opts.event - True to make a MatrixEvent.
* @returns The event
*/
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
@ -250,13 +250,13 @@ export interface IMessageOpts {
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@ -290,14 +290,14 @@ interface IReplyMessageOpts extends IMessageOpts {
/**
* Create a reply message.
*
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {MatrixEvent} opts.replyToMessage The replied message
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.replyToMessage - The replied message
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@ -329,10 +329,8 @@ export function mkReplyMessage(
/**
* A mock implementation of webstorage
*
* @constructor
*/
export class MockStorageApi {
export class MockStorageApi implements Storage {
private data: Record<string, any> = {};
public get length() {
@ -354,30 +352,39 @@ export class MockStorageApi {
public removeItem(k: string): void {
delete this.data[k];
}
public clear(): void {
this.data = {};
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
* @returns promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
export async function awaitDecryption(
event: MatrixEvent, { waitOnDecryptionFailure = false } = {},
): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
} else {
return event;
}
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
}
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));

View File

@ -26,6 +26,7 @@ import {
MatrixClient,
MatrixEvent,
Room,
RoomMember,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
@ -33,7 +34,7 @@ import {
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call";
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { CallFeed } from "../../src/webrtc/callFeed";
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
@ -83,6 +84,17 @@ export const DUMMY_SDP = (
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
export const FAKE_ROOM_ID = "!fake:test.dummy";
export const FAKE_CONF_ID = "fakegroupcallid";
export const FAKE_USER_ID_1 = "@alice:test.dummy";
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
export const FAKE_SESSION_ID_1 = "alice1";
export const FAKE_USER_ID_2 = "@bob:test.dummy";
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
export const FAKE_SESSION_ID_2 = "bob1";
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
class MockMediaStreamAudioSourceNode {
public connect() {}
}
@ -103,12 +115,14 @@ export class MockRTCPeerConnection {
private negotiationNeededListener?: () => void;
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public iceConnectionStateChangeListener?: () => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable";
public iceConnectionState: RTCIceConnectionState = "connected";
public transceivers: MockRTCRtpTransceiver[] = [];
public static triggerAllNegotiations(): void {
@ -144,6 +158,8 @@ export class MockRTCPeerConnection {
this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') {
this.iceCandidateListener = listener;
} else if (type === 'iceconnectionstatechange') {
this.iceConnectionStateChangeListener = listener;
} else if (type == 'track') {
this.onTrackListener = listener;
}
@ -416,6 +432,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public supportsExperimentalThreads(): boolean { return true; }
public async decryptEventIfNeeded(): Promise<void> {}
public typed(): MatrixClient { return this as unknown as MatrixClient; }
public emitRoomState(event: MatrixEvent, state: RoomState): void {
@ -428,6 +447,43 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
}
}
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
constructor(public roomId: string, public groupCallId?: string) {
super();
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"),
};
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public on = jest.fn();
public removeListener = jest.fn();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string | undefined {
return this.opponentDeviceId;
}
public typed(): MatrixCall { return this as unknown as MatrixCall; }
}
export class MockCallFeed {
constructor(
public userId: string,

View File

@ -3,7 +3,7 @@ import '../olm-loader';
import { EventEmitter } from "events";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { MatrixClient } from "../../src/client";
import { IClaimOTKsResult, MatrixClient } from "../../src/client";
import { Crypto } from "../../src/crypto";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../MockStorageApi";
@ -19,19 +19,20 @@ import { MemoryStore } from "../../src";
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
import { RoomMember } from '../../src/models/room-member';
import { IStore } from '../../src/store';
import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList";
const Olm = global.Olm;
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
return new Promise((resolve) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent> {
const roomId = event.getRoomId();
async function keyshareEventForEvent(client: MatrixClient, event: MatrixEvent, index?: number): Promise<MatrixEvent> {
const roomId = event.getRoomId()!;
const eventContent = event.getWireContent();
const key = await client.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
@ -41,16 +42,16 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: client.getUserId(),
sender: client.getUserId()!,
content: {
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"sender_key": eventContent.sender_key,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
"sender_claimed_ed25519_key": key?.sender_claimed_ed25519_key,
"session_id": eventContent.session_id,
"session_key": key.key,
"chain_index": key.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
"session_key": key?.key,
"chain_index": key?.chain_index,
"forwarding_curve25519_key_chain": key?.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
});
@ -171,7 +172,8 @@ describe("Crypto", function() {
});
describe('Session management', function() {
const otkResponse = {
const otkResponse: IClaimOTKsResult = {
failures: {},
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
@ -187,11 +189,12 @@ describe("Crypto", function() {
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
let crypto: Crypto;
let mockBaseApis: MatrixClient;
let mockRoomList: RoomList;
let fakeEmitter: EventEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi() as unknown as Storage;
@ -218,8 +221,8 @@ describe("Crypto", function() {
sendToDevice: jest.fn(),
getKeyBackupVersion: jest.fn(),
isGuest: jest.fn(),
};
mockRoomList = {};
} as unknown as MatrixClient;
mockRoomList = {} as unknown as RoomList;
fakeEmitter = new EventEmitter();
@ -232,7 +235,7 @@ describe("Crypto", function() {
mockRoomList,
[],
);
crypto.registerEventHandlers(fakeEmitter);
crypto.registerEventHandlers(fakeEmitter as any);
await crypto.init();
});
@ -244,7 +247,7 @@ describe("Crypto", function() {
const prom = new Promise<void>((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
return Promise.resolve(otkResponse);
};
});
@ -988,16 +991,22 @@ describe("Crypto", function() {
ensureOlmSessionsForDevices.mockResolvedValue({});
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => {
result.plaintext = JSON.stringify(payload);
result.plaintext = { type: 0, body: JSON.stringify(payload) };
});
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
// running initCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([
client.client.initCrypto(),
client.httpBackend.flush("/keys/upload", 1),
]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: JSON.stringify(payload) },
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
};
});
@ -1009,18 +1018,24 @@ describe("Crypto", function() {
it("encrypts and sends to devices", async () => {
client.httpBackend
.when("PUT", "/sendToDevice/m.room.encrypted", {
messages: {
"@bob:example.org": {
bobweb: encryptedPayload,
bobmobile: encryptedPayload,
.when("PUT", "/sendToDevice/m.room.encrypted")
.check((request) => {
const data = request.data;
delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"];
delete data.messages["@bob:example.org"]["bobmobile"]["org.matrix.msgid"];
delete data.messages["@carol:example.org"]["caroldesktop"]["org.matrix.msgid"];
expect(data).toStrictEqual({
messages: {
"@bob:example.org": {
bobweb: encryptedPayload,
bobmobile: encryptedPayload,
},
"@carol:example.org": {
caroldesktop: encryptedPayload,
},
},
"@carol:example.org": {
caroldesktop: encryptedPayload,
},
},
})
.respond(200, {});
});
}).respond(200, {});
await Promise.all([
client.client.encryptAndSendToDevices(
@ -1039,13 +1054,18 @@ describe("Crypto", function() {
encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => {
// Refuse to encrypt to Carol's desktop device
if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return;
result.plaintext = JSON.stringify(payload);
result.plaintext = { type: 0, body: JSON.stringify(payload) };
});
client.httpBackend
.when("PUT", "/sendToDevice/m.room.encrypted", {
.when("PUT", "/sendToDevice/m.room.encrypted")
.check((req) => {
const data = req.data;
delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"];
// Carol is nowhere to be seen
messages: { "@bob:example.org": { bobweb: encryptedPayload } },
expect(data).toStrictEqual({
messages: { "@bob:example.org": { bobweb: encryptedPayload } },
});
})
.respond(200, {});
@ -1140,4 +1160,48 @@ describe("Crypto", function() {
await client!.client.crypto!.start();
});
});
describe("setRoomEncryption", () => {
let mockClient: MatrixClient;
let mockRoomList: RoomList;
let clientStore: IStore;
let crypto: Crypto;
beforeEach(async function() {
mockClient = {} as MatrixClient;
const mockStorage = new MockStorageApi() as unknown as Storage;
clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
const cryptoStore = new MemoryCryptoStore();
mockRoomList = {
getRoomEncryption: jest.fn().mockReturnValue(null),
setRoomEncryption: jest.fn().mockResolvedValue(undefined),
} as unknown as RoomList;
crypto = new Crypto(
mockClient,
"@alice:home.server",
"FLIBBLE",
clientStore,
cryptoStore,
mockRoomList,
[],
);
});
it("should set the algorithm if called for a known room", async () => {
const room = new Room("!room:id", mockClient, "@my.user:id");
await clientStore.storeRoom(room);
await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption);
expect(mockRoomList!.setRoomEncryption).toHaveBeenCalledTimes(1);
expect(jest.mocked(mockRoomList!.setRoomEncryption).mock.calls[0][0]).toEqual("!room:id");
});
it("should raise if called for an unknown room", async () => {
await expect(async () => {
await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption);
}).rejects.toThrow(/unknown room/);
expect(mockRoomList!.setRoomEncryption).not.toHaveBeenCalled();
});
});
});

View File

@ -232,7 +232,7 @@ describe.each([
return store;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store;
let store: IndexedDBCryptoStore;
beforeAll(() => {
store = dbFactory();

View File

@ -22,6 +22,7 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
import { DeviceList } from "../../../src/crypto/DeviceList";
import { IDownloadKeyResult, MatrixClient } from "../../../src";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { CryptoStore } from "../../../src/crypto/store/base";
const signedDeviceList: IDownloadKeyResult = {
"failures": {},
@ -88,8 +89,8 @@ const signedDeviceList2: IDownloadKeyResult = {
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
let downloadSpy: jest.Mock;
let cryptoStore: CryptoStore;
let deviceLists: DeviceList[] = [];
beforeEach(function() {
@ -112,7 +113,7 @@ describe('DeviceList', function() {
deviceId: 'HGKAWHRVJQ',
} as unknown as MatrixClient;
const mockOlm = {
verifySignature: function(key, message, signature) {},
verifySignature: function(key: string, message: string, signature: string) {},
} as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);

View File

@ -17,6 +17,7 @@ limitations under the License.
import { mocked, MockedObject } from 'jest-mock';
import '../../../olm-loader';
import type { OutboundGroupSession } from "@matrix-org/olm";
import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import * as testUtils from "../../../test-utils/test-utils";
@ -31,6 +32,7 @@ import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter';
import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
@ -87,7 +89,7 @@ describe("MegolmDecryption", function() {
});
describe('receives some keys:', function() {
let groupSession;
let groupSession: OutboundGroupSession;
beforeEach(async function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
@ -298,10 +300,10 @@ describe("MegolmDecryption", function() {
describe("session reuse and key reshares", () => {
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
let megolmEncryption: MegolmEncryptionClass;
let aliceDeviceInfo: DeviceInfo;
let mockRoom: Room;
let olmDevice: OlmDevice;
beforeEach(async () => {
// @ts-ignore assigning to readonly prop
@ -342,7 +344,7 @@ describe("MegolmDecryption", function() {
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
};
} as unknown as DeviceInfo;
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
@ -365,7 +367,7 @@ describe("MegolmDecryption", function() {
algorithm: 'm.megolm.v1.aes-sha2',
rotation_period_ms: rotationPeriodMs,
},
});
}) as MegolmEncryptionClass;
// Splice the real method onto the mock object as megolm uses this method
// on the crypto class in order to encrypt / start sessions
@ -381,7 +383,7 @@ describe("MegolmDecryption", function() {
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
} as unknown as Room;
});
it("should use larger otkTimeout when preparing to encrypt room", async () => {
@ -397,11 +399,14 @@ describe("MegolmDecryption", function() {
});
it("should generate a new session if this one needs rotation", async () => {
// @ts-ignore - private method access
const session = await megolmEncryption.prepareNewSession(false);
session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time
// Inject expired session which needs rotation
// @ts-ignore - private field access
megolmEncryption.setupPromise = Promise.resolve(session);
// @ts-ignore - private method access
const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession");
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
@ -446,8 +451,8 @@ describe("MegolmDecryption", function() {
});
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
await megolmEncryption.reshareKeyWithDevice!(
olmDevice.deviceCurve25519Key!,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
@ -466,8 +471,8 @@ describe("MegolmDecryption", function() {
);
mockBaseApis.queueToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
await megolmEncryption.reshareKeyWithDevice!(
olmDevice.deviceCurve25519Key!,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
@ -558,7 +563,9 @@ describe("MegolmDecryption", function() {
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
delete contentMap["@bob:example.com"].bobdevice2.session_id;
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
@ -755,6 +762,7 @@ describe("MegolmDecryption", function() {
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {

View File

@ -31,17 +31,21 @@ function makeOlmDevice() {
return olmDevice;
}
async function setupSession(initiator, opponent) {
async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey);
return sid;
}
function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
describe("OlmDevice", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
@ -159,11 +163,6 @@ describe("OlmDevice", function() {
}, "ABCDEFG"),
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
@ -235,12 +234,6 @@ describe("OlmDevice", function() {
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB,
));

View File

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from "jest-mock";
import '../../olm-loader';
import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
@ -30,7 +28,10 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { MatrixScheduler } from '../../../src';
import { IndexedDBCryptoStore, MatrixScheduler } from '../../../src';
import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm;
@ -102,32 +103,39 @@ const CURVE25519_BACKUP_INFO = {
},
};
const AES256_BACKUP_INFO = {
const AES256_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1',
auth_data: {
// FIXME: add iv and mac
},
auth_data: {} as IKeyBackupInfo["auth_data"],
};
const keys = {};
const keys: Record<string, Uint8Array> = {};
function getCrossSigningKey(type) {
return keys[type];
function getCrossSigningKey(type: string) {
return Promise.resolve(keys[type]);
}
function saveCrossSigningKeys(k) {
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k);
}
function makeTestClient(cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
function makeTestScheduler(): MatrixScheduler {
return ([
"getQueueForEvent",
"queueEvent",
"removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixScheduler);
}
function makeTestClient(cryptoStore: CryptoStore) {
const scheduler = makeTestScheduler();
const store = new StubStore();
return new MatrixClient({
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
@ -139,6 +147,10 @@ function makeTestClient(cryptoStore) {
cryptoStore: cryptoStore,
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
return client;
}
describe("MegolmBackup", function() {
@ -151,36 +163,33 @@ describe("MegolmBackup", function() {
return Olm.init();
});
let olmDevice;
let mockOlmLib;
let mockCrypto;
let cryptoStore;
let megolmDecryption;
let olmDevice: OlmDevice;
let mockOlmLib: typeof olmlib;
let mockCrypto: Crypto;
let cryptoStore: CryptoStore;
let megolmDecryption: MegolmDecryptionClass;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
// @ts-ignore making mock
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib = {} as unknown as typeof olmlib;
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined);
});
describe("backup", function() {
let mockBaseApis;
let mockBaseApis: MatrixClient;
beforeEach(function() {
mockBaseApis = {};
mockBaseApis = {} as unknown as MatrixClient;
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@ -188,8 +197,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
@ -239,6 +249,7 @@ describe("MegolmBackup", function() {
};
mockCrypto.cancelRoomKeyRequest = function() {};
// @ts-ignore readonly field write
mockCrypto.backupManager = {
backupGroupSession: jest.fn(),
};
@ -264,21 +275,22 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
[IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
@ -298,25 +310,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
client.http.authedRequest = function(
method, path, queryParams, data, opts,
): Promise<T> {
): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -343,8 +355,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
@ -354,13 +367,13 @@ describe("MegolmBackup", function() {
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
[IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
@ -381,25 +394,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
client.http.authedRequest = function(
method, path, queryParams, data, opts,
): Promise<T> {
): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -426,8 +439,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
@ -437,10 +451,10 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await Promise.all([
new Promise<void>((resolve, reject) => {
let backupInfo;
let backupInfo: Record<string, any> | BodyInit | undefined;
client.http.authedRequest = function(
method, path, queryParams, data, opts,
) {
): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) {
@ -486,10 +500,7 @@ describe("MegolmBackup", function() {
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@ -502,6 +513,8 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore: cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@ -509,20 +522,21 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
[IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
@ -542,26 +556,26 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
client.http.authedRequest = function(
method, path, queryParams, data, opts,
): Promise<T> {
): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
@ -579,7 +593,7 @@ describe("MegolmBackup", function() {
});
describe("restore", function() {
let client;
let client: MatrixClient;
beforeEach(function() {
client = makeTestClient(cryptoStore);
@ -590,8 +604,9 @@ describe("MegolmBackup", function() {
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
@ -603,7 +618,7 @@ describe("MegolmBackup", function() {
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
@ -620,7 +635,7 @@ describe("MegolmBackup", function() {
it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA);
return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
@ -637,7 +652,7 @@ describe("MegolmBackup", function() {
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
return Promise.resolve<any>({
rooms: {
[ROOM_ID]: {
sessions: {
@ -649,7 +664,7 @@ describe("MegolmBackup", function() {
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, CURVE25519_BACKUP_INFO,
null!, null!, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
@ -659,18 +674,18 @@ describe("MegolmBackup", function() {
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
await client.crypto!.storeSessionBackupPrivateKey(key);
const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result!)).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
@ -679,7 +694,7 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
@ -688,7 +703,7 @@ describe("MegolmBackup", function() {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(
@ -702,10 +717,7 @@ describe("MegolmBackup", function() {
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 scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@ -718,6 +730,9 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);

View File

@ -18,23 +18,24 @@ limitations under the License.
import '../../olm-loader';
import anotherjson from 'another-json';
import { PkSigning } from '@matrix-org/olm';
import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
import { CryptoEvent } from '../../../src/crypto';
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from '../../../src/client';
import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto';
import { IDevice } from '../../../src/crypto/deviceinfo';
import { TestClient } from '../../TestClient';
import { resetCrossSigningKeys } from "./crypto-utils";
const PUSH_RULES_RESPONSE = {
const PUSH_RULES_RESPONSE: Response = {
method: "GET",
path: "/pushrules/",
data: {},
};
const filterResponse = function(userId) {
const filterResponse = function(userId: string): Response {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
@ -43,7 +44,13 @@ const filterResponse = function(userId) {
};
};
function setHttpResponses(httpBackend, responses) {
interface Response {
method: 'GET' | 'PUT' | 'POST' | 'DELETE';
path: string;
data: object;
}
function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
@ -54,13 +61,13 @@ function setHttpResponses(httpBackend, responses) {
async function makeTestClient(
userInfo: { userId: string, deviceId: string},
options: Partial<ICreateClientOpts> = {},
keys = {},
keys: Record<string, Uint8Array> = {},
) {
function getCrossSigningKey(type) {
return keys[type];
function getCrossSigningKey(type: string) {
return keys[type] ?? null;
}
function saveCrossSigningKeys(k) {
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k);
}
@ -142,7 +149,9 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async func => {
await func({});
};
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
// through failure, stopping before actually applying changes.
@ -275,7 +284,7 @@ describe("Cross Signing", function() {
);
// feed sync result that includes master key, ssk, device key
const responses = [
const responses: Response[] = [
PUSH_RULES_RESPONSE,
{
method: "POST",
@ -464,7 +473,7 @@ describe("Cross Signing", function() {
});
it.skip("should trust signatures received from other devices", async function() {
const aliceKeys: Record<string, PkSigning> = {};
const aliceKeys: Record<string, Uint8Array> = {};
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
undefined,
@ -494,8 +503,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
@ -549,7 +557,7 @@ describe("Cross Signing", function() {
// - ssk
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
const responses: Response[] = [
PUSH_RULES_RESPONSE,
{
method: "POST",
@ -853,7 +861,7 @@ describe("Cross Signing", function() {
});
it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc;
let upgradeResolveFunc: Function;
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@ -1129,3 +1137,67 @@ describe("Cross Signing", function() {
alice.stopClient();
});
});
describe("userHasCrossSigningKeys", function() {
if (!global.Olm) {
return;
}
beforeAll(() => {
return global.Olm.init();
});
let aliceClient: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(async () => {
const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
aliceClient = testClient.client;
httpBackend = testClient.httpBackend;
});
afterEach(() => {
aliceClient.stopClient();
});
it("should download devices and return true if one is a cross-signing key", async () => {
httpBackend
.when("POST", "/keys/query")
.respond(200, {
"master_keys": {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
},
},
});
let result: boolean;
await Promise.all([
httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}),
]);
expect(result!).toBeTruthy();
});
it("should download devices and return false if there is no cross-signing key", async () => {
httpBackend
.when("POST", "/keys/query")
.respond(200, {});
let result: boolean;
await Promise.all([
httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}),
]);
expect(result!).toBeFalsy();
});
it("throws an error if crypto is disabled", () => {
aliceClient.crypto = undefined;
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
});
});

View File

@ -1,14 +1,16 @@
import { IRecoveryKey } from '../../../src/crypto/api';
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { MatrixClient } from "../../../src";
import { CryptoEvent } from "../../../src/crypto";
// needs to be phased out and replaced with bootstrapSecretStorage,
// but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys(
client,
client: MatrixClient,
{ level }: { level?: CrossSigningLevel} = {},
): Promise<void> {
const crypto = client.crypto;
const crypto = client.crypto!;
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
try {
@ -28,7 +30,8 @@ export async function resetCrossSigningKeys(
crypto.crossSigningInfo.keys = oldKeys;
throw e;
}
crypto.emit("crossSigning.keysChanged", {});
crypto.emit(CryptoEvent.KeysChanged, {});
// @ts-ignore
await crypto.afterCrossSigningLocalKeyChange();
}

View File

@ -16,6 +16,7 @@ limitations under the License.
import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib";
import { IObject } from "../../../src/crypto/olmlib";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient';
@ -23,9 +24,11 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger';
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
@ -48,9 +51,15 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
// Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object.
function sign(obj, key, userId) {
function sign<T extends IObject | ICurve25519AuthData>(obj: T, key: Uint8Array, userId: string): T & {
signatures: ISignatures;
unsigned?: object;
} {
olmlib.pkSign(obj, key, userId, '');
return obj;
return obj as T & {
signatures: ISignatures;
unsigned?: object;
};
}
describe("Secrets", function() {
@ -169,12 +178,12 @@ describe("Secrets", function() {
return [newKeyId, key];
});
let keys = {};
let keys: Record<string, Uint8Array> = {};
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
getCrossSigningKey: t => Promise.resolve(keys[t]),
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
@ -227,7 +236,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
expect(secretName).toBe("foo");
return "bar";
return Promise.resolve("bar");
},
},
},
@ -354,7 +363,7 @@ describe("Secrets", function() {
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
const bob: MatrixClient = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
@ -364,15 +373,15 @@ describe("Secrets", function() {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
return [defaultKeyId!, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} });
bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
@ -380,16 +389,19 @@ describe("Secrets", function() {
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
bob.crypto.backupManager.checkKeyBackup = async () => {};
bob.crypto!.backupManager.checkKeyBackup = async () => null;
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto!.secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async func => {
await func({});
},
});
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
@ -400,13 +412,15 @@ describe("Secrets", function() {
});
// Clear local cross-signing keys and read from secret storage
bob.crypto.deviceList.storeCrossSigningForUser(
bob.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async func => {
await func({});
},
});
expect(crossSigning.getId()).toBeTruthy();
@ -422,7 +436,7 @@ describe("Secrets", function() {
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
@ -498,14 +512,14 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
self_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
@ -557,7 +571,7 @@ describe("Secrets", function() {
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
@ -642,14 +656,14 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
self_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_signing: sign<ICrossSigningKey>({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {

View File

@ -18,12 +18,12 @@ import "../../../olm-loader";
import { makeTestClients } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { DeviceInfo, IDevice } 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 as Verification, VerificationBase } from "../../../../src/crypto/verification/Base";
import { 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";
@ -31,8 +31,8 @@ import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
let ALICE_DEVICES;
let BOB_DEVICES;
let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function() {
if (!global.Olm) {
@ -75,7 +75,7 @@ describe("SAS verification", function() {
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: Verification<any, any>;
let aliceVerifier: SAS;
let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void;
@ -95,25 +95,25 @@ describe("SAS verification", function() {
ALICE_DEVICES = {
Osborne2: {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Osborne2": aliceDevice.deviceEd25519Key,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key,
"ed25519:Osborne2": aliceDevice.deviceEd25519Key!,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
BOB_DEVICES = {
Dynabook: {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice.deviceEd25519Key,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
@ -136,7 +136,7 @@ describe("SAS verification", function() {
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@ -158,7 +158,7 @@ describe("SAS verification", function() {
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
);
) as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
@ -413,7 +413,7 @@ 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) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
e.mismatch();
});
resolve(request.verifier!);
@ -443,13 +443,13 @@ describe("SAS verification", function() {
});
describe("verification in DM", function() {
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: SAS;
let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function;
beforeEach(async function() {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@ -477,7 +477,7 @@ describe("SAS verification", function() {
);
};
alice.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
bob.client.crypto!.setDeviceVerification = jest.fn();
@ -495,16 +495,16 @@ describe("SAS verification", function() {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => {
bob.client.on(CryptoEvent.VerificationRequest, async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME) as SAS;
verifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@ -525,12 +525,10 @@ describe("SAS verification", function() {
});
});
const aliceRequest = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id",
);
const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => {
aliceVerifier = aliceRequest.verifier! as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {

View File

@ -125,7 +125,7 @@ describe("self-verifications", () => {
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub);
expect(result![0][0]).toBe(testKeyPub);
expect(result![1][0]).toBe(testKeyPub);
});
});

View File

@ -0,0 +1,60 @@
/*
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 { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
describe("crypto.setDeviceVerification", () => {
const userId = "@alice:example.com";
const deviceId1 = "device1";
let client: MatrixClient;
if (!CRYPTO_ENABLED) {
return;
}
beforeAll(async () => {
await Olm.init();
});
beforeEach(async () => {
client = (new TestClient(userId, deviceId1)).client;
await client.initCrypto();
});
it("client should provide crypto", () => {
expect(client.crypto).not.toBeUndefined();
});
describe("when setting an own device as verified", () => {
beforeEach(async () => {
jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests");
await client.crypto!.setDeviceVerification(
userId,
deviceId1,
true,
);
});
it("cancelAndResendAllOutgoingKeyRequests should be called", () => {
expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled();
});
});
});

View File

@ -16,13 +16,21 @@ limitations under the License.
*/
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client';
import { MatrixClient, ClientEvent, ICreateClientOpts } from '../../../../src/client';
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
interface UserInfo {
userId: string;
deviceId: string;
}
export async function makeTestClients(
userInfos: UserInfo[],
options: Partial<ICreateClientOpts>,
): Promise<[TestClient[], () => void]> {
const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {};
@ -51,7 +59,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
}
return {};
};
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => {
// make up a unique ID as the event ID
const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = {
@ -88,11 +96,12 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
};
for (const userInfo of userInfos) {
let keys = {};
let keys: Record<string, Uint8Array> = {};
if (!options) options = {};
if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
if (!options.cryptoCallbacks.saveCrossSigningKeys) {
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
// @ts-ignore tsc getting confused by overloads
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
}
const testClient = new TestClient(
@ -104,6 +113,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
}
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
// @ts-ignore tsc getting confused by overloads
testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient);
}

View File

@ -18,7 +18,7 @@ import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
@ -30,12 +30,12 @@ type MockClient = MatrixClient & {
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events: MatrixEvent[] = [];
const deviceEvents = {};
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
return {
getUserId() { return userId; },
getDeviceId() { return deviceId; },
sendEvent(roomId, type, content) {
sendEvent(roomId: string, type: string, content: IContent) {
counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({
@ -49,7 +49,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return Promise.resolve({ event_id: eventId });
},
sendToDevice(type, msgMap) {
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
@ -111,7 +111,7 @@ class MockVerifier extends VerificationBase<'', any> {
}
}
async handleEvent(event) {
async handleEvent(event: MatrixEvent) {
if (event.getType() === DONE_TYPE && !this._startEvent) {
await this._channel.send(DONE_TYPE, {});
}
@ -122,7 +122,7 @@ class MockVerifier extends VerificationBase<'', any> {
}
}
function makeRemoteEcho(event) {
function makeRemoteEcho(event: MatrixEvent) {
return new MatrixEvent(Object.assign({}, event.event, {
unsigned: {
transaction_id: "abc",

View File

@ -17,14 +17,8 @@ limitations under the License.
import { mocked } from "jest-mock";
import { logger } from "../../src/logger";
import { MatrixClient, ClientEvent } from "../../src/client";
import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
import { Filter } from "../../src/filter";
import {
Method,
ClientPrefix,
IRequestOpts,
} from "../../src/http-api";
import { QueryDict } from "../../src/utils";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
EventType,
@ -42,7 +36,18 @@ 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, MatrixError, Room } from "../../src";
import {
ContentHelpers,
ClientPrefix,
Direction,
EventTimeline, ICreateRoomOpts,
IRequestOpts,
MatrixError,
MatrixHttpApi,
MatrixScheduler,
Method,
Room,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
@ -50,6 +55,9 @@ import {
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope,
} from "../../src/models/invites-ignorer";
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
import { QueryDict } from "../../src/utils";
import { SyncState } from "../../src/sync";
jest.useFakeTimers();
@ -71,17 +79,37 @@ function convertQueryDictToStringRecord(queryDict?: QueryDict): Record<string, s
}, {} as Record<string, string>);
}
type HttpLookup = {
method: string;
path: string;
prefix?: string;
data?: Record<string, any>;
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
thenCall?: Function;
};
interface Options extends ICreateRoomOpts {
_roomId?: string;
}
type WrappedRoom = Room & {
_options: Options;
_state: Map<string, any>;
};
describe("MatrixClient", function() {
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server";
let client;
let store;
let scheduler;
let client: MatrixClient;
let store: Store;
let scheduler: MatrixScheduler;
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
const PUSH_RULES_RESPONSE = {
const PUSH_RULES_RESPONSE: HttpLookup = {
method: "GET",
path: "/pushrules/",
data: {},
@ -89,7 +117,7 @@ describe("MatrixClient", function() {
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
const FILTER_RESPONSE = {
const FILTER_RESPONSE: HttpLookup = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" },
@ -101,23 +129,14 @@ describe("MatrixClient", function() {
rooms: {},
};
const SYNC_RESPONSE = {
const SYNC_RESPONSE: HttpLookup = {
method: "GET",
path: "/sync",
data: SYNC_DATA,
};
// items are popped off when processed and block if no items left.
let httpLookups: {
method: string;
path: string;
prefix?: string;
data?: object;
error?: object;
expectBody?: object;
expectQueryParams?: QueryDict;
thenCall?: Function;
}[] = [];
let httpLookups: HttpLookup[] = [];
let acceptKeepalives: boolean;
let pendingLookup: {
promise: Promise<any>;
@ -128,7 +147,7 @@ describe("MatrixClient", function() {
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
body?: BodyInit,
requestOpts: IRequestOpts = {},
) {
const { prefix } = requestOpts;
@ -224,24 +243,38 @@ describe("MatrixClient", function() {
userId: userId,
});
// FIXME: We shouldn't be yanking http like this.
client.http = [
"authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
client.http.authedRequest.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
client.http = ([
"authedRequest",
"getContentUri",
"request",
"uploadContent",
] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixHttpApi<any>);
mocked(client.http.authedRequest).mockImplementation(httpReq);
mocked(client.http.request).mockImplementation(httpReq);
}
beforeEach(function() {
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
scheduler = ([
"getQueueForEvent",
"queueEvent",
"removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
store = [
] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixScheduler);
store = ([
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
"startup", "deleteAllData",
] as const).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as Store);
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
@ -265,7 +298,7 @@ describe("MatrixClient", function() {
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client.http.authedRequest.mockImplementation(function() {
mocked(client.http.authedRequest).mockImplementation(function() {
return new Promise(() => {});
});
client.stopClient();
@ -287,10 +320,10 @@ describe("MatrixClient", function() {
},
}];
await client.timestampToEvent(roomId, 0, 'f');
await client.timestampToEvent(roomId, 0, Direction.Forward);
expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1);
const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0];
expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1);
const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toStrictEqual('GET');
expect(prefix).toStrictEqual(ClientPrefix.V1);
expect(path).toStrictEqual(
@ -326,16 +359,16 @@ describe("MatrixClient", function() {
},
}];
await client.timestampToEvent(roomId, 0, 'f');
await client.timestampToEvent(roomId, 0, Direction.Forward);
expect(client.http.authedRequest.mock.calls.length).toStrictEqual(2);
expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(2);
const [
stableMethod,
stablePath,
stableQueryParams,
,
{ prefix: stablePrefix },
] = client.http.authedRequest.mock.calls[0];
] = mocked(client.http.authedRequest).mock.calls[0];
expect(stableMethod).toStrictEqual('GET');
expect(stablePrefix).toStrictEqual(ClientPrefix.V1);
expect(stablePath).toStrictEqual(
@ -352,7 +385,7 @@ describe("MatrixClient", function() {
unstableQueryParams,
,
{ prefix: unstablePrefix },
] = client.http.authedRequest.mock.calls[1];
] = mocked(client.http.authedRequest).mock.calls[1];
expect(unstableMethod).toStrictEqual('GET');
expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix);
expect(unstablePath).toStrictEqual(
@ -379,10 +412,10 @@ describe("MatrixClient", function() {
},
}];
await expect(client.timestampToEvent(roomId, 0, 'f')).rejects.toBeDefined();
await expect(client.timestampToEvent(roomId, 0, Direction.Forward)).rejects.toBeDefined();
expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1);
const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0];
expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1);
const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toStrictEqual('GET');
expect(prefix).toStrictEqual(ClientPrefix.V1);
expect(path).toStrictEqual(
@ -453,7 +486,7 @@ describe("MatrixClient", function() {
const txnId = client.makeTxnId();
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
@ -493,7 +526,7 @@ describe("MatrixClient", function() {
};
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
@ -523,7 +556,7 @@ describe("MatrixClient", function() {
const userId = "@test:example.org";
const roomId = "!room:example.org";
const roomName = "Test Tree";
const mockRoom = {};
const mockRoom = {} as unknown as Room;
const fn = jest.fn().mockImplementation((opts) => {
expect(opts).toMatchObject({
name: roomName,
@ -595,23 +628,23 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key");
}
},
},
};
} as Room["currentState"],
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeDefined();
expect(tree.roomId).toEqual(roomId);
expect(tree.room).toBe(mockRoom);
expect(tree!.roomId).toEqual(roomId);
expect(tree!.room).toBe(mockRoom);
});
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
};
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
@ -655,8 +688,8 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key");
}
},
},
};
} as Room["currentState"],
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
@ -689,8 +722,8 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key");
}
},
},
};
} as Room["currentState"],
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
@ -705,15 +738,15 @@ describe("MatrixClient", function() {
SYNC_RESPONSE,
];
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
mocked(store.getFilterIdByName).mockReturnValue(filterId);
const filter = new Filter("0", filterId);
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
store.getFilter.mockReturnValue(filter);
mocked(store.getFilter).mockReturnValue(filter);
const syncPromise = new Promise<void>((resolve, reject) => {
client.on("sync", function syncListener(state) {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
resolve();
} else if (state === "ERROR") {
reject(new Error("sync error"));
@ -731,10 +764,10 @@ describe("MatrixClient", function() {
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise<void>((resolve) => {
client.on("sync", function syncListener(state) {
client.on(ClientEvent.Sync, function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
resolve();
}
});
@ -750,7 +783,7 @@ describe("MatrixClient", function() {
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix?: string) {
function getFilterName(userId: string, suffix?: string) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
@ -769,14 +802,14 @@ describe("MatrixClient", function() {
},
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.mockReturnValue(invalidFilterId);
mocked(store.getFilterIdByName).mockReturnValue(invalidFilterId);
const filterName = getFilterName(client.credentials.userId);
const filterName = getFilterName(client.credentials.userId!);
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
done();
});
});
@ -798,13 +831,13 @@ describe("MatrixClient", function() {
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
done();
} else {
// unexpected state transition!
@ -822,7 +855,7 @@ describe("MatrixClient", function() {
method: "GET", path: "/sync", data: SYNC_DATA,
});
client.on("sync", function syncListener(state) {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
@ -832,7 +865,7 @@ describe("MatrixClient", function() {
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jest.advanceTimersByTime(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
done();
}
});
@ -848,13 +881,13 @@ describe("MatrixClient", function() {
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
done();
} else {
// unexpected state transition!
@ -866,8 +899,8 @@ describe("MatrixClient", function() {
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
function syncChecker(expectedStates: [string, string | null][], done: Function) {
return function syncListener(state: SyncState, old: SyncState | null) {
const expected = expectedStates.shift();
logger.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
@ -879,7 +912,7 @@ describe("MatrixClient", function() {
expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) {
client.removeListener("sync", syncListener);
client.removeListener(ClientEvent.Sync, syncListener);
done();
}
// standard retry time is 5 to 10 seconds
@ -890,7 +923,7 @@ describe("MatrixClient", function() {
it("should transition null -> PREPARED after the first /sync", function(done) {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -902,7 +935,7 @@ describe("MatrixClient", function() {
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -932,7 +965,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -940,7 +973,7 @@ describe("MatrixClient", function() {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -959,7 +992,7 @@ describe("MatrixClient", function() {
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -973,7 +1006,7 @@ describe("MatrixClient", function() {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -985,7 +1018,7 @@ describe("MatrixClient", function() {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
@ -1009,7 +1042,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
});
});
@ -1078,14 +1111,14 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key");
}
},
},
} as Room["currentState"],
getThread: jest.fn(),
addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(),
reEmitter: {
reEmit: jest.fn(),
},
};
} as unknown as Room;
beforeEach(() => {
client.getRoom = (getRoomId) => {
@ -1151,7 +1184,7 @@ describe("MatrixClient", function() {
const mockRoom = {
getMyMembership: () => "join",
updatePendingEvent: (event, status) => event.setStatus(status),
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
@ -1168,15 +1201,14 @@ describe("MatrixClient", function() {
throw new Error("Unexpected event type or state key");
}
},
},
};
} as Room["currentState"],
} as unknown as Room;
let event;
let event: MatrixEvent;
beforeEach(async () => {
event = new MatrixEvent({
event_id: "~" + roomId + ":" + txnId,
user_id: client.credentials.userId,
sender: client.credentials.userId,
sender: client.credentials.userId!,
room_id: roomId,
origin_server_ts: new Date().getTime(),
});
@ -1187,25 +1219,26 @@ describe("MatrixClient", function() {
return mockRoom;
};
client.crypto = { // mock crypto
encryptEvent: (event, room) => new Promise(() => {}),
encryptEvent: () => new Promise(() => {}),
stop: jest.fn(),
};
} as unknown as Crypto;
});
function assertCancelled() {
expect(event.status).toBe(EventStatus.CANCELLED);
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy();
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
}
it("should cancel an event which is queued", () => {
event.setStatus(EventStatus.QUEUED);
client.scheduler.queueEvent(event);
client.scheduler?.queueEvent(event);
client.cancelPendingEvent(event);
assertCancelled();
});
it("should cancel an event which is encrypting", async () => {
// @ts-ignore protected method access
client.encryptAndSendEvent(null, event);
await testUtils.emitPromise(event, "Event.status");
client.cancelPendingEvent(event);
@ -1267,7 +1300,7 @@ describe("MatrixClient", function() {
const room = {
hasPendingEvent: jest.fn().mockReturnValue(false),
addLocalEchoReceipt: jest.fn(),
};
} as unknown as Room;
const rrEvent = new MatrixEvent({ event_id: "read_event_id" });
const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" });
client.getRoom = () => room;
@ -1306,7 +1339,7 @@ describe("MatrixClient", function() {
const content = makeBeaconInfoContent(100, true);
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
mocked(client.http.authedRequest).mockClear().mockResolvedValue({});
});
it("creates new beacon info", async () => {
@ -1314,7 +1347,7 @@ describe("MatrixClient", function() {
// event type combined
const expectedEventType = M_BEACON_INFO.name;
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
@ -1328,7 +1361,7 @@ describe("MatrixClient", function() {
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
@ -1406,7 +1439,7 @@ describe("MatrixClient", function() {
const newPassword = 'newpassword';
const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0];
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
@ -1414,7 +1447,7 @@ describe("MatrixClient", function() {
};
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
mocked(client.http.authedRequest).mockClear().mockResolvedValue({});
});
it("no logout_devices specified", async () => {
@ -1453,13 +1486,13 @@ describe("MatrixClient", function() {
const response = {
aliases: ["#woop:example.org", "#another:example.org"],
};
client.http.authedRequest.mockClear().mockResolvedValue(response);
mocked(client.http.authedRequest).mockClear().mockResolvedValue(response);
const roomId = "!whatever:example.org";
const result = await client.getLocalAliases(roomId);
// Current version of the endpoint we support is v3
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0];
expect(data).toBeFalsy();
expect(method).toBe('GET');
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
@ -1516,11 +1549,11 @@ describe("MatrixClient", function() {
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
} as unknown as ITurnServerResponse;
jest.spyOn(client, "turnServer").mockResolvedValue(turnServer);
const events: any[][] = [];
const onTurnServers = (...args) => events.push(args);
const onTurnServers = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServers, onTurnServers);
expect(await client.checkTurnServers()).toBe(true);
client.off(ClientEvent.TurnServers, onTurnServers);
@ -1536,7 +1569,7 @@ describe("MatrixClient", function() {
jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args);
const onTurnServersError = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError);
@ -1548,7 +1581,7 @@ describe("MatrixClient", function() {
jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args);
const onTurnServersError = (...args: any[]) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError);
@ -1564,7 +1597,7 @@ describe("MatrixClient", function() {
it("is an alias for the crypto method", async () => {
client.crypto = testUtils.mock(Crypto, "Crypto");
const deviceInfos = [];
const deviceInfos: IOlmDevice[] = [];
const payload = {};
await client.encryptAndSendToDevices(deviceInfos, payload);
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
@ -1577,7 +1610,7 @@ describe("MatrixClient", function() {
const dataStore = new Map();
client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content);
return Promise.resolve();
return Promise.resolve({});
};
client.getAccountData = function(eventType) {
const data = dataStore.get(eventType);
@ -1588,9 +1621,9 @@ describe("MatrixClient", function() {
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map();
client.createRoom = function(options = {}) {
client.createRoom = function(options: Options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map();
const state = new Map<string, any>();
const room = {
roomId,
_options: options,
@ -1608,24 +1641,24 @@ describe("MatrixClient", function() {
},
};
},
};
} as EventTimeline;
},
};
},
};
} as unknown as WrappedRoom;
rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId });
};
client.getRoom = function(roomId) {
return rooms.get(roomId);
};
client.joinRoom = function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
client.joinRoom = async function(roomId) {
return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts);
};
// Mockup state events
client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId);
const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state;
let store = state.get(type);
if (!store) {
@ -1644,14 +1677,15 @@ describe("MatrixClient", function() {
return content;
},
};
return { event_id: eventId };
return Promise.resolve({ event_id: eventId });
};
client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId);
const room = this.getRoom(roomId) as WrappedRoom;
const state: Map<string, any> = room._state;
for (const store of state.values()) {
delete store[eventId];
delete store[eventId!];
}
return Promise.resolve({ event_id: "$" + eventId + "-" + Math.random() });
};
});
@ -1687,7 +1721,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
@ -1716,7 +1750,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org",
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
expect(ruleSenderMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
@ -1726,7 +1760,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:example.org",
});
expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({
expect(ruleRoomMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
@ -1751,7 +1785,7 @@ describe("MatrixClient", function() {
roomId: BAD_ROOM_ID,
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
expect(ruleSenderMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
@ -1785,7 +1819,7 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
@ -1813,13 +1847,13 @@ describe("MatrixClient", function() {
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
expect(ruleMatch!.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch);
await client.ignoredInvites.removeRule(ruleMatch as MatrixEvent);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
@ -1833,10 +1867,10 @@ describe("MatrixClient", function() {
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID) as WrappedRoom;
// Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent();
expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy();
@ -1850,7 +1884,7 @@ describe("MatrixClient", function() {
expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent();
expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy();
@ -1862,7 +1896,7 @@ describe("MatrixClient", function() {
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId);
const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../../src";
import { IContent, MatrixClient } from "../../../src";
import { Room } from "../../../src/models/room";
import { MatrixEvent } from "../../../src/models/event";
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event";
@ -33,7 +33,7 @@ describe("MSC3089TreeSpace", () => {
const roomId = "!tree:localhost";
const targetUser = "@target:example.org";
let powerLevels;
let powerLevels: MatrixEvent;
beforeEach(() => {
// TODO: Use utility functions to create test rooms and clients
@ -480,7 +480,7 @@ describe("MSC3089TreeSpace", () => {
const staticDomain = "static.example.org";
function addSubspace(roomId: string, createTs?: number, order?: string) {
const content = {
const content: IContent = {
via: [staticDomain],
};
if (order) content['order'] = order;

View File

@ -121,7 +121,7 @@ describe('MatrixEvent', () => {
});
describe(".attemptDecryption", () => {
let encryptedEvent;
let encryptedEvent: MatrixEvent;
const eventId = 'test_encrypted_event';
beforeEach(() => {
@ -155,7 +155,7 @@ describe('MatrixEvent', () => {
},
});
}),
};
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);

View File

@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../../src/client";
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
import { Room } from "../../../src/models/room";
import { Thread } from "../../../src/models/thread";
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
import { mkThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
import { emitPromise, mkMessage } from "../../test-utils/test-utils";
import { EventStatus } from "../../../src";
describe('Thread', () => {
describe("constructor", () => {
@ -30,6 +32,50 @@ describe('Thread', () => {
});
});
it("includes pending events in replyCount", async () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(
myUserId,
"DEVICE",
"ACCESS_TOKEN",
undefined,
{ timelineSupport: false },
);
const client = testClient.client;
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: ["@alice:example.org"],
length: 3,
});
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
const event = mkMessage({
room: room.roomId,
user: myUserId,
msg: "thread reply",
relatesTo: {
rel_type: THREAD_RELATION_TYPE.name,
event_id: thread.id,
},
event: true,
});
await thread.processEvent(event);
event.setStatus(EventStatus.SENDING);
room.addPendingEvent(event, "txn01");
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(3);
});
describe("hasUserReadEvent", () => {
const myUserId = "@bob:example.org";
let client: MatrixClient;

View File

@ -37,9 +37,9 @@ let event: MatrixEvent;
let threadEvent: MatrixEvent;
const ROOM_ID = "!roomId:example.org";
let THREAD_ID;
let THREAD_ID: string;
function mkPushAction(notify, highlight): IActionsObject {
function mkPushAction(notify: boolean, highlight: boolean): IActionsObject {
return {
notify,
tweaks: {
@ -76,7 +76,7 @@ describe("fixNotificationCountOnDecryption", () => {
event: true,
}, mockClient);
THREAD_ID = event.getId();
THREAD_ID = event.getId()!;
threadEvent = mkEvent({
type: EventType.RoomMessage,
content: {
@ -108,6 +108,16 @@ describe("fixNotificationCountOnDecryption", () => {
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("does not change the room count when there's no unread count", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("changes the thread count to highlight on decryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
@ -118,6 +128,16 @@ describe("fixNotificationCountOnDecryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
});
it("does not change the room count when there's no unread count", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("emits events", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);

View File

@ -20,6 +20,7 @@ import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts'
import { MatrixClient } from "../../src/client";
import { Feature, ServerSupport } from '../../src/feature';
import { EventType } from '../../src/matrix';
import { synthesizeReceipt } from '../../src/models/read-receipt';
import { encodeUri } from '../../src/utils';
import * as utils from "../test-utils/test-utils";
@ -69,7 +70,7 @@ const roomEvent = utils.mkEvent({
},
});
function mockServerSideSupport(client, serverSideSupport: ServerSupport) {
function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) {
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
}
@ -175,4 +176,20 @@ describe("Read receipt", () => {
await flushPromises();
});
});
describe("synthesizeReceipt", () => {
it.each([
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
const userId = "@bob:example.org";
const receiptType = ReceiptType.Read;
const fakeReadReceipt = synthesizeReceipt(userId, event, receiptType);
const content = fakeReadReceipt.getContent()[event.getId()!][receiptType][userId];
expect(content.thread_id).toEqual(destinationId);
});
});
});

View File

@ -20,7 +20,7 @@ let wallTime = 1234567890;
jest.useFakeTimers().setSystemTime(wallTime);
describe("realtime-callbacks", function() {
function tick(millis) {
function tick(millis: number): void {
wallTime += millis;
jest.advanceTimersByTime(millis);
}

View File

@ -87,7 +87,7 @@ describe("Rendezvous", function() {
});
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetchFn;
let fetchFn: typeof global.fetch;
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
beforeEach(function() {

View File

@ -373,37 +373,36 @@ describe("RoomMember", function() {
expect(member.events.member).toEqual(joinEvent);
});
it("should set 'name' based on user_id, displayname and room state",
function() {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should set 'name' based on user_id, displayname and room state", function() {
const roomState = {
getStateEvents: function(type: string) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName: string) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
let emitCount = 0;
@ -455,7 +454,7 @@ describe("RoomMember", function() {
});
const roomState = {
getStateEvents: function(type) {
getStateEvents: function(type: string) {
if (type !== "m.room.member") {
return [];
}
@ -467,7 +466,7 @@ describe("RoomMember", function() {
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
getUserIdsWithDisplayName: function(displayName: string) {
return [userA, userC];
},
} as unknown as RoomState;

View File

@ -16,30 +16,34 @@ limitations under the License.
/**
* This is an internal module. See {@link MatrixClient} for the public class.
* @module client
*/
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { emitPromise } from "../test-utils/test-utils";
import {
Direction,
DuplicateStrategy,
EventStatus,
EventTimelineSet,
EventType, IStateEventWithRoomId,
EventType, IContent,
IStateEventWithRoomId,
JoinRule,
MatrixEvent,
MatrixEventEvent,
PendingEventOrdering,
RelationType,
RoomEvent,
RoomMember,
} from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { NotificationCountType, Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
import { Crypto } from "../../src/crypto";
describe("Room", function() {
@ -48,7 +52,7 @@ describe("Room", function() {
const userB = "@bertha:bar";
const userC = "@clarissa:bar";
const userD = "@dorothy:bar";
let room;
let room: Room;
const mkMessage = () => utils.mkMessage({
event: true,
@ -131,13 +135,16 @@ describe("Room", function() {
beforeEach(function() {
room = new Room(roomId, new TestClient(userA, "device").client, userA);
// mock RoomStates
// @ts-ignore
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
// @ts-ignore
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
});
describe('getCreator', () => {
it("should return the creator from m.room.create", function() {
room.currentState.getStateEvents.mockImplementation(function(type, key) {
// @ts-ignore - mocked doesn't handle overloads sanely
mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) {
if (type === EventType.RoomCreate && key === "") {
return utils.mkEvent({
event: true,
@ -160,7 +167,8 @@ describe("Room", function() {
const hsUrl = "https://my.home.server";
it("should return the URL from m.room.avatar preferentially", function() {
room.currentState.getStateEvents.mockImplementation(function(type, key) {
// @ts-ignore - mocked doesn't handle overloads sanely
mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) {
if (type === EventType.RoomAvatar && key === "") {
return utils.mkEvent({
event: true,
@ -174,10 +182,10 @@ describe("Room", function() {
});
}
});
const url = room.getAvatarUrl(hsUrl);
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale");
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return nothing if there is no m.room.avatar and allowDefault=false",
@ -189,12 +197,12 @@ describe("Room", function() {
describe("getMember", function() {
beforeEach(function() {
room.currentState.getMember.mockImplementation(function(userId) {
mocked(room.currentState.getMember).mockImplementation(function(userId) {
return {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
} as unknown as RoomMember,
}[userId] || null;
});
});
@ -222,11 +230,13 @@ describe("Room", function() {
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow();
// @ts-ignore
expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow();
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() {
// @ts-ignore
room.addLiveEvents(events, {
duplicateStrategy: "foo",
});
@ -255,6 +265,7 @@ describe("Room", function() {
dupe.event.event_id = events[0].getId();
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
// @ts-ignore
room.addLiveEvents([dupe], {
duplicateStrategy: "ignore",
});
@ -263,7 +274,7 @@ describe("Room", function() {
it("should emit 'Room.timeline' events", function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
@ -306,8 +317,8 @@ describe("Room", function() {
userId: userA,
membership: "join",
name: "Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
} as unknown as RoomMember;
mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
@ -331,27 +342,25 @@ describe("Room", function() {
const remoteEventId = remoteEvent.getId();
let callCount = 0;
room.on("Room.localEchoUpdated",
function(event, emitRoom, oldEventId, oldStatus) {
switch (callCount) {
case 0:
expect(event.getId()).toEqual(localEventId);
expect(event.status).toEqual(EventStatus.SENDING);
expect(emitRoom).toEqual(room);
expect(oldEventId).toBeUndefined();
expect(oldStatus).toBeUndefined();
break;
case 1:
expect(event.getId()).toEqual(remoteEventId);
expect(event.status).toBeNull();
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
expect(oldStatus).toBe(EventStatus.SENDING);
break;
}
callCount += 1;
},
);
room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => {
switch (callCount) {
case 0:
expect(event.getId()).toEqual(localEventId);
expect(event.status).toEqual(EventStatus.SENDING);
expect(emitRoom).toEqual(room);
expect(oldEventId).toBeUndefined();
expect(oldStatus).toBeUndefined();
break;
case 1:
expect(event.getId()).toEqual(remoteEventId);
expect(event.status).toBeNull();
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
expect(oldStatus).toBe(EventStatus.SENDING);
break;
}
callCount += 1;
});
// first add the local echo
room.addPendingEvent(localEvent, "TXN_ID");
@ -367,7 +376,7 @@ describe("Room", function() {
it("should be able to update local echo without a txn ID (/send then /sync)", function() {
const eventJson = utils.mkMessage({
room: roomId, user: userA, event: false,
}) as object;
});
delete eventJson["txn_id"];
delete eventJson["event_id"];
const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson));
@ -398,7 +407,7 @@ describe("Room", function() {
it("should be able to update local echo without a txn ID (/sync then /send)", function() {
const eventJson = utils.mkMessage({
room: roomId, user: userA, event: false,
}) as object;
});
delete eventJson["txn_id"];
delete eventJson["event_id"];
const txnId = "My_txn_id";
@ -483,7 +492,7 @@ describe("Room", function() {
it("should emit 'Room.timeline' events when added to the start",
function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
@ -501,19 +510,19 @@ describe("Room", function() {
userId: userA,
membership: "join",
name: "Alice",
};
} as unknown as RoomMember;
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
} as unknown as RoomMember;
mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@ -539,19 +548,19 @@ describe("Room", function() {
userId: userA,
membership: "join",
name: "Alice",
};
} as unknown as RoomMember;
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
} as unknown as RoomMember;
mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@ -599,7 +608,7 @@ describe("Room", function() {
});
});
const resetTimelineTests = function(timelineSupport) {
const resetTimelineTests = function(timelineSupport: boolean) {
let events: MatrixEvent[];
beforeEach(function() {
@ -630,8 +639,8 @@ describe("Room", function() {
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
expect(oldState.getStateEvents(EventType.RoomName, "")).toEqual(events[1]);
expect(newState.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
expect(oldState?.getStateEvents(EventType.RoomName, "")).toEqual(events[1]);
expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]);
});
it("should reset the legacy timeline fields", function() {
@ -669,17 +678,14 @@ describe("Room", function() {
expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0);
});
it("should emit Room.timelineReset event and set the correct " +
"pagination token", function() {
it("should emit Room.timelineReset event and set the correct pagination token", function() {
let callCount = 0;
room.on("Room.timelineReset", function(emitRoom) {
room.on(RoomEvent.TimelineReset, function(emitRoom) {
callCount += 1;
expect(emitRoom).toEqual(room);
// make sure that the pagination token has been set before the
// event is emitted.
const tok = emitRoom.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
// make sure that the pagination token has been set before the event is emitted.
const tok = emitRoom?.getLiveTimeline().getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("pagToken");
});
@ -693,7 +699,7 @@ describe("Room", function() {
const firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
const tl = room.getTimelineForEvent(events[0].getId());
const tl = room.getTimelineForEvent(events[0].getId()!);
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
});
};
@ -721,30 +727,25 @@ describe("Room", function() {
it("should handle events in the same timeline", function() {
room.addLiveEvents(events);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
.toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!,
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, events[1].getId()!))
.toBeGreaterThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[1].getId()!))
.toEqual(0);
});
it("should handle events in adjacent timelines", function() {
const oldTimeline = room.addTimeline();
oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), 'f');
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b');
oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward);
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward);
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
.toBeLessThan(0);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!))
.toBeGreaterThan(0);
});
@ -754,11 +755,9 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!,
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!))
.toBe(null);
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!,
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!))
.toBe(null);
});
@ -769,21 +768,21 @@ describe("Room", function() {
.compareEventOrdering(events[0].getId()!, "xxx"))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId()))
.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);
});
});
describe("getJoinedMembers", function() {
it("should return members whose membership is 'join'", function() {
room.currentState.getMembers.mockImplementation(function() {
mocked(room.currentState.getMembers).mockImplementation(function() {
return [
{ userId: "@alice:bar", membership: "join" },
{ userId: "@bob:bar", membership: "invite" },
{ userId: "@cleo:bar", membership: "leave" },
{ userId: "@alice:bar", membership: "join" } as unknown as RoomMember,
{ userId: "@bob:bar", membership: "invite" } as unknown as RoomMember,
{ userId: "@cleo:bar", membership: "leave" } as unknown as RoomMember,
];
});
const res = room.getJoinedMembers();
@ -792,9 +791,9 @@ describe("Room", function() {
});
it("should return an empty list if no membership is 'join'", function() {
room.currentState.getMembers.mockImplementation(function() {
mocked(room.currentState.getMembers).mockImplementation(function() {
return [
{ userId: "@bob:bar", membership: "invite" },
{ userId: "@bob:bar", membership: "invite" } as unknown as RoomMember,
];
});
const res = room.getJoinedMembers();
@ -805,41 +804,41 @@ describe("Room", function() {
describe("hasMembershipState", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.getMember.mockImplementation(function(userId) {
mocked(room.currentState.getMember).mockImplementation(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
}[userId];
}[userId] as unknown as RoomMember;
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.getMember.mockImplementation(function(userId) {
mocked(room.currentState.getMember).mockImplementation(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
}[userId] as unknown as RoomMember;
});
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.getMember.mockImplementation(function(userId) {
mocked(room.currentState.getMember).mockImplementation(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
}[userId] as unknown as RoomMember;
});
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.getMember.mockImplementation(function(userId) {
mocked(room.currentState.getMember).mockImplementation(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
}[userId] as unknown as RoomMember;
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
@ -1193,8 +1192,8 @@ describe("Room", function() {
event: true,
});
function mkReceipt(roomId: string, records) {
const content = {};
function mkReceipt(roomId: string, records: Array<ReturnType<typeof mkRecord>>) {
const content: IContent = {};
records.forEach(function(r) {
if (!content[r.eventId]) {
content[r.eventId] = {};
@ -1241,7 +1240,7 @@ describe("Room", function() {
it("should emit an event when a receipt is added",
function() {
const listener = jest.fn();
room.on("Room.receipt", listener);
room.on(RoomEvent.Receipt, listener);
const ts = 13787898424;
@ -1448,7 +1447,7 @@ describe("Room", function() {
});
describe("tags", function() {
function mkTags(roomId, tags) {
function mkTags(roomId: string, tags: object) {
const content = { "tags": tags };
return new MatrixEvent({
content: content,
@ -1470,7 +1469,7 @@ describe("Room", function() {
"received on the event stream",
function() {
const listener = jest.fn();
room.on("Room.tags", listener);
room.on(RoomEvent.Tags, listener);
const tags = { "m.foo": { "order": 0.5 } };
const event = mkTags(roomId, tags);
@ -1642,11 +1641,14 @@ describe("Room", function() {
});
describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) {
function createClientMock(
serverResponse: Error | MatrixEvent[],
storageResponse: MatrixEvent[] | Error | null = null,
) {
return {
getEventMapper: function() {
// events should already be MatrixEvents
return function(event) {return event;};
return function(event: MatrixEvent) {return event;};
},
isCryptoEnabled() {
return true;
@ -1671,7 +1673,7 @@ describe("Room", function() {
return Promise.resolve(this.storageResponse);
}
},
setOutOfBandMembers: function(roomId, memberEvents) {
setOutOfBandMembers: function(roomId: string, memberEvents: IStateEventWithRoomId[]) {
this.storedMembers = memberEvents;
return Promise.resolve();
},
@ -2170,7 +2172,7 @@ describe("Room", function() {
},
});
room.createThread("$000", undefined, [eventWithoutARootEvent]);
room.createThread("$000", undefined, [eventWithoutARootEvent], false);
const rootEvent = new MatrixEvent({
event_id: "$666",
@ -2188,7 +2190,7 @@ describe("Room", function() {
},
});
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow();
expect(() => room.createThread(rootEvent.getId()!, rootEvent, [], false)).not.toThrow();
});
it("creating thread from edited event should not conflate old versions of the event", () => {
@ -2406,8 +2408,6 @@ describe("Room", function() {
Thread.setServerSideListSupport(FeatureSupport.Stable);
room.client.createThreadListMessagesRequest = () => Promise.resolve({
start: null,
end: null,
chunk: [],
state: [],
});
@ -2761,7 +2761,7 @@ describe("Room", function() {
});
describe("thread notifications", () => {
let room;
let room: Room;
beforeEach(() => {
const client = new TestClient(userA).client;

View File

@ -1,18 +1,19 @@
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
import { defer } from '../../src/utils';
import { defer, IDeferred } from '../../src/utils';
import { MatrixError } from "../../src/http-api";
import { MatrixScheduler } from "../../src/scheduler";
import * as utils from "../test-utils/test-utils";
import { MatrixEvent } from "../../src";
jest.useFakeTimers();
describe("MatrixScheduler", function() {
let scheduler;
let retryFn;
let queueFn;
let deferred;
let scheduler: MatrixScheduler<Record<string, boolean>>;
let retryFn: Function | null;
let queueFn: ((event: MatrixEvent) => string | null) | null;
let deferred: IDeferred<Record<string, boolean>>;
const roomId = "!foo:bar";
const eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
@ -65,8 +66,8 @@ describe("MatrixScheduler", function() {
deferB.resolve({ b: true });
deferA.resolve({ a: true });
const [a, b] = await abPromise;
expect(a.a).toEqual(true);
expect(b.b).toEqual(true);
expect(a!.a).toEqual(true);
expect(b!.b).toEqual(true);
});
it("should invoke the retryFn on failure and wait the amount of time specified",
@ -92,6 +93,7 @@ describe("MatrixScheduler", function() {
return new Promise(() => {});
}
expect(procCount).toBeLessThan(3);
return new Promise(() => {});
});
scheduler.queueEvent(eventA);
@ -119,8 +121,8 @@ describe("MatrixScheduler", function() {
return "yep";
};
const deferA = defer();
const deferB = defer();
const deferA = defer<Record<string, boolean>>();
const deferB = defer<Record<string, boolean>>();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@ -132,6 +134,7 @@ describe("MatrixScheduler", function() {
return deferB.promise;
}
expect(procCount).toBeLessThan(3);
return new Promise<Record<string, boolean>>(() => {});
});
const globalA = scheduler.queueEvent(eventA);
@ -159,7 +162,7 @@ describe("MatrixScheduler", function() {
const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true });
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
const buckets = {};
const buckets: Record<string, string> = {};
buckets[eventA.getId()!] = "queue_A";
buckets[eventD.getId()!] = "queue_A";
buckets[eventB.getId()!] = "queue_B";
@ -169,13 +172,13 @@ describe("MatrixScheduler", function() {
return 0;
};
queueFn = function(event) {
return buckets[event.getId()];
return buckets[event.getId()!];
};
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
const deferA = defer<void>();
const deferA = defer<Record<string, boolean>>();
scheduler.setProcessFunction(function(event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
@ -191,7 +194,7 @@ describe("MatrixScheduler", function() {
// wait a bit then resolve A and we should get D (not C) next.
setTimeout(function() {
deferA.resolve();
deferA.resolve({});
}, 1000);
jest.advanceTimersByTime(1000);
});
@ -210,7 +213,7 @@ describe("MatrixScheduler", function() {
};
const prom = scheduler.queueEvent(eventA);
expect(prom).toBeTruthy();
expect(prom.then).toBeTruthy();
expect(prom!.then).toBeTruthy();
});
});
@ -237,15 +240,15 @@ describe("MatrixScheduler", function() {
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
const queue = scheduler.getQueueForEvent(eventA);
expect(queue.length).toEqual(2);
expect(queue).toHaveLength(2);
expect(queue).toEqual([eventA, eventB]);
// modify the queue
const eventC = utils.mkMessage(
{ user: "@a:bar", room: roomId, event: true },
);
queue.push(eventC);
queue!.push(eventC);
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain.length).toEqual(2);
expect(queueAgain).toHaveLength(2);
});
it("should return a list of events in the queue and modifications to" +
@ -255,10 +258,10 @@ describe("MatrixScheduler", function() {
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
const queue = scheduler.getQueueForEvent(eventA);
queue[1].event.content.body = "foo";
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain[1].event.content.body).toEqual("foo");
const queue = scheduler.getQueueForEvent(eventA)!;
queue[1].event.content!.body = "foo";
const queueAgain = scheduler.getQueueForEvent(eventA)!;
expect(queueAgain[1].event.content?.body).toEqual("foo");
});
});

View File

@ -16,7 +16,8 @@ limitations under the License.
*/
import { ReceiptType } from "../../src/@types/read_receipts";
import { SyncAccumulator } from "../../src/sync-accumulator";
import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
// The event body & unsigned object get frozen to assert that they don't get altered
// by the impl
@ -55,10 +56,10 @@ const RES_WITH_AGE = {
},
},
},
};
} as unknown as ISyncResponse;
describe("SyncAccumulator", function() {
let sa;
let sa: SyncAccumulator;
beforeEach(function() {
sa = new SyncAccumulator({
@ -98,7 +99,7 @@ describe("SyncAccumulator", function() {
},
},
},
};
} as unknown as ISyncResponse;
sa.accumulate(res);
const output = sa.getJSON();
expect(output.nextBatch).toEqual(res.next_batch);
@ -223,7 +224,6 @@ describe("SyncAccumulator", function() {
content: {
user_ids: ["@alice:localhost"],
},
room_id: "!foo:bar",
}],
},
});
@ -281,12 +281,12 @@ describe("SyncAccumulator", function() {
account_data: {
events: [acc1],
},
});
} as unknown as ISyncResponse);
sa.accumulate({
account_data: {
events: [acc2],
},
});
} as unknown as ISyncResponse);
expect(
sa.getJSON().accountData.length,
).toEqual(1);
@ -422,7 +422,7 @@ describe("SyncAccumulator", function() {
});
describe("summary field", function() {
function createSyncResponseWithSummary(summary) {
function createSyncResponseWithSummary(summary: IRoomSummary): ISyncResponse {
return {
next_batch: "abc",
rooms: {
@ -444,7 +444,7 @@ describe("SyncAccumulator", function() {
},
},
},
};
} as unknown as ISyncResponse;
}
afterEach(() => {
@ -487,8 +487,8 @@ describe("SyncAccumulator", function() {
jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual(
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned!.age! + delta,
);
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
@ -507,12 +507,12 @@ describe("SyncAccumulator", function() {
sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"]
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
.unread_thread_notifications!["$143273582443PhrSn:example.org"]).not.toBeUndefined();
});
});
});
function syncSkeleton(joinObj) {
function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
joinObj = joinObj || {};
return {
next_batch: "abc",
@ -521,11 +521,12 @@ function syncSkeleton(joinObj) {
"!foo:bar": joinObj,
},
},
};
} as unknown as ISyncResponse;
}
function msg(localpart, text) {
function msg(localpart: string, text: string) {
return {
event_id: "$" + Math.random(),
content: {
body: text,
},
@ -535,8 +536,9 @@ function msg(localpart, text) {
};
}
function member(localpart, membership) {
function member(localpart: string, membership: string) {
return {
event_id: "$" + Math.random(),
content: {
membership: membership,
},

View File

@ -455,7 +455,11 @@ describe("utils", function() {
describe("recursivelyAssign", () => {
it("doesn't override with null/undefined", () => {
const result = utils.recursivelyAssign(
const result = utils.recursivelyAssign<{
string: string;
object: object;
float: number;
}, {}>(
{
string: "Hello world",
object: {},
@ -475,7 +479,11 @@ describe("utils", function() {
});
it("assigns recursively", () => {
const result = utils.recursivelyAssign(
const result = utils.recursivelyAssign<{
number: number;
object: object;
thing: string | object;
}, {}>(
{
number: 42,
object: {

View File

@ -933,7 +933,7 @@ describe('Call', function() {
await fakeIncomingCall(client, call, "1");
});
const untilEventSent = async (...args) => {
const untilEventSent = async (...args: any[]) => {
const maxTries = 20;
for (let tries = 0; tries < maxTries; ++tries) {
@ -971,7 +971,7 @@ describe('Call', function() {
});
describe("ICE candidate sending", () => {
let mockPeerConn;
let mockPeerConn: MockRTCPeerConnection;
const fakeCandidateString = "here is a fake candidate!";
const fakeCandidateEvent = {
candidate: {
@ -1086,7 +1086,7 @@ describe('Call', function() {
});
describe("Screen sharing", () => {
const waitNegotiateFunc = resolve => {
const waitNegotiateFunc = (resolve: Function): void => {
mockSendEvent.mockImplementationOnce(() => {
// Note that the peer connection here is a dummy one and always returns
// dummy SDP, so there's not much point returning the content: the SDP will
@ -1392,7 +1392,7 @@ describe('Call', function() {
it("ends call on onHangupReceived() if state is ringing", async () => {
expect(call.callHasEnded()).toBe(false);
call.state = CallState.Ringing;
(call as any).state = CallState.Ringing;
call.onHangupReceived({} as MCallHangupReject);
expect(call.callHasEnded()).toBe(true);
@ -1424,7 +1424,7 @@ describe('Call', function() {
)("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => {
expect(call.callHasEnded()).toBe(false);
call.state = state;
(call as any).state = state;
call.onRejectReceived({} as MCallHangupReject);
expect(call.callHasEnded()).toBe(
@ -1458,4 +1458,50 @@ describe('Call', function() {
expect(call.hasPeerConnection).toBe(true);
});
});
it("should correctly emit LengthChanged", async () => {
const advanceByArray = [2, 3, 5];
const lengthChangedListener = jest.fn();
jest.useFakeTimers();
call.addListener(CallEvent.LengthChanged, lengthChangedListener);
await fakeIncomingCall(client, call, "1");
(call.peerConn as unknown as MockRTCPeerConnection).iceConnectionStateChangeListener!();
let hasAdvancedBy = 0;
for (const advanceBy of advanceByArray) {
jest.advanceTimersByTime(advanceBy * 1000);
hasAdvancedBy += advanceBy;
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
}
});
describe("ICE disconnected timeout", () => {
let mockPeerConn: MockRTCPeerConnection;
beforeEach(async () => {
jest.useFakeTimers();
jest.spyOn(call, "hangup");
await fakeIncomingCall(client, call, "1");
mockPeerConn = (call.peerConn as unknown as MockRTCPeerConnection);
mockPeerConn.iceConnectionState = "disconnected";
mockPeerConn.iceConnectionStateChangeListener!();
});
it("should hang up after being disconnected for 30 seconds", () => {
jest.advanceTimersByTime(31 * 1000);
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
});
it("should not hangup if we've managed to re-connect", () => {
mockPeerConn.iceConnectionState = "connected";
mockPeerConn.iceConnectionStateChangeListener!();
jest.advanceTimersByTime(31 * 1000);
expect(call.hangup).not.toHaveBeenCalled();
});
});
});

View File

@ -17,13 +17,30 @@ limitations under the License.
import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { TestClient } from "../../TestClient";
import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
import { MockMatrixCall, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
import { CallEvent, CallState } from "../../../src/webrtc/call";
describe("CallFeed", () => {
let client;
const roomId = "room1";
let client: TestClient;
let call: MockMatrixCall;
let feed: CallFeed;
beforeEach(() => {
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
call = new MockMatrixCall(roomId);
feed = new CallFeed({
client: client.client,
call: call.typed(),
roomId,
userId: "user1",
// @ts-ignore Mock
stream: new MockMediaStream("stream1"),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
});
});
afterEach(() => {
@ -31,21 +48,6 @@ describe("CallFeed", () => {
});
describe("muting", () => {
let feed: CallFeed;
beforeEach(() => {
feed = new CallFeed({
client,
roomId: "room1",
userId: "user1",
// @ts-ignore Mock
stream: new MockMediaStream("stream1"),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
});
});
describe("muting by default", () => {
it("should mute audio by default", () => {
expect(feed.isAudioMuted()).toBeTruthy();
@ -86,4 +88,23 @@ describe("CallFeed", () => {
});
});
});
describe("connected", () => {
it.each([true, false])("should always be connected, if isLocal()", (val: boolean) => {
// @ts-ignore
feed._connected = val;
jest.spyOn(feed, "isLocal").mockReturnValue(true);
expect(feed.connected).toBeTruthy();
});
it.each([
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
call.emit(CallEvent.State, state);
expect(feed.connected).toBe(expected);
});
});
});

View File

@ -25,7 +25,7 @@ import {
} from '../../../src';
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
import { MatrixClient } from "../../../src/client";
import { IMyDevice, MatrixClient } from "../../../src/client";
import {
installWebRTCMocks,
MockCallFeed,
@ -33,6 +33,16 @@ import {
MockMediaStream,
MockMediaStreamTrack,
MockRTCPeerConnection,
MockMatrixCall,
FAKE_ROOM_ID,
FAKE_USER_ID_1,
FAKE_CONF_ID,
FAKE_DEVICE_ID_2,
FAKE_SESSION_ID_2,
FAKE_USER_ID_2,
FAKE_DEVICE_ID_1,
FAKE_SESSION_ID_1,
FAKE_USER_ID_3,
} from '../../test-utils/webrtc';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { sleep } from "../../../src/utils";
@ -41,16 +51,6 @@ import { CallFeed } from '../../../src/webrtc/callFeed';
import { CallEvent, CallState } from '../../../src/webrtc/call';
import { flushPromises } from '../../test-utils/flushPromises';
const FAKE_ROOM_ID = "!fake:test.dummy";
const FAKE_CONF_ID = "fakegroupcallid";
const FAKE_USER_ID_1 = "@alice:test.dummy";
const FAKE_DEVICE_ID_1 = "@AAAAAA";
const FAKE_SESSION_ID_1 = "alice1";
const FAKE_USER_ID_2 = "@bob:test.dummy";
const FAKE_DEVICE_ID_2 = "@BBBBBB";
const FAKE_SESSION_ID_2 = "bob1";
const FAKE_USER_ID_3 = "@charlie:test.dummy";
const FAKE_STATE_EVENTS = [
{
getContent: () => ({
@ -123,42 +123,6 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<G
return groupCall;
};
class MockCall {
constructor(public roomId: string, public groupCallId: string) {
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"),
};
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public on = jest.fn();
public removeListener = jest.fn();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string {
return this.opponentDeviceId;
}
public typed(): MatrixCall { return this as unknown as MatrixCall; }
}
describe('Group Call', function() {
beforeEach(function() {
installWebRTCMocks();
@ -180,13 +144,13 @@ describe('Group Call', function() {
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
});
it("does not initialize local call feed, if it already is", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.initLocalCallFeed();
jest.spyOn(groupCall, "initLocalCallFeed");
await groupCall.enter();
@ -216,10 +180,6 @@ describe('Group Call', function() {
});
it("sends member state event to room on enter", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.create();
try {
@ -249,10 +209,6 @@ describe('Group Call', function() {
});
it("sends member state event to room on leave", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.create();
await groupCall.enter();
mockSendState.mockClear();
@ -267,6 +223,18 @@ describe('Group Call', function() {
);
});
it("includes local device in participants when entered via another session", async () => {
const hasLocalParticipant = () => groupCall.participants.get(
room.getMember(mockClient.getUserId()!)!,
)?.has(mockClient.getDeviceId()!) ?? false;
expect(groupCall.enteredViaAnotherSession).toBe(false);
expect(hasLocalParticipant()).toBe(false);
groupCall.enteredViaAnotherSession = true;
expect(hasLocalParticipant()).toBe(true);
});
it("starts with mic unmuted in regular calls", async () => {
try {
await groupCall.create();
@ -347,7 +315,7 @@ describe('Group Call', function() {
});
describe("call feeds changing", () => {
let call: MockCall;
let call: MockMatrixCall;
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new"));
@ -357,13 +325,13 @@ describe('Group Call', function() {
jest.spyOn(groupCall, "emit");
call = new MockCall(room.roomId, groupCall.groupCallId);
call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
await groupCall.create();
});
it("ignores changes, if we can't get user id of opponent", async () => {
const call = new MockCall(room.roomId, groupCall.groupCallId);
const call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined });
// @ts-ignore Mock
@ -510,10 +478,11 @@ describe('Group Call', function() {
});
it("sends metadata updates before unmuting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId);
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
// @ts-ignore
groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]),
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
);
let metadataUpdateResolve: () => void;
@ -535,10 +504,11 @@ describe('Group Call', function() {
});
it("sends metadata updates after muting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId);
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
// @ts-ignore
groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]),
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
);
// the call starts muted, so unmute to get in the right state to test
@ -694,6 +664,7 @@ describe('Group Call', function() {
expect(client1.sendToDevice).toHaveBeenCalled();
// @ts-ignore
const oldCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
@ -715,6 +686,7 @@ describe('Group Call', function() {
// to even be created...
let newCall: MatrixCall | undefined;
while (
// @ts-ignore
(newCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)?.get(client2.deviceId)) === undefined
@ -759,6 +731,7 @@ describe('Group Call', function() {
groupCall1.setMicrophoneMuted(false);
groupCall1.setLocalVideoMuted(false);
// @ts-ignore
const call = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
@ -870,7 +843,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
// @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [
@ -893,7 +869,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
// @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [
@ -935,7 +914,7 @@ describe('Group Call', function() {
});
it("ignores incoming calls for other rooms", async () => {
const mockCall = new MockCall("!someotherroom.fake.dummy", groupCall.groupCallId);
const mockCall = new MockMatrixCall("!someotherroom.fake.dummy", groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@ -944,7 +923,7 @@ describe('Group Call', function() {
});
it("rejects incoming calls for the wrong group call", async () => {
const mockCall = new MockCall(room.roomId, "not " + groupCall.groupCallId);
const mockCall = new MockMatrixCall(room.roomId, "not " + groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@ -952,7 +931,7 @@ describe('Group Call', function() {
});
it("ignores incoming calls not in the ringing state", async () => {
const mockCall = new MockCall(room.roomId, groupCall.groupCallId);
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockCall.state = CallState.Connected;
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
@ -962,12 +941,13 @@ describe('Group Call', function() {
});
it("answers calls for the right room & group call ID", async () => {
const mockCall = new MockCall(room.roomId, groupCall.groupCallId);
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
expect(mockCall.reject).not.toHaveBeenCalled();
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, mockCall]]),
@ -975,8 +955,8 @@ describe('Group Call', function() {
});
it("replaces calls if it already has one with the same user", async () => {
const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockCall(room.roomId, groupCall.groupCallId);
const oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
newMockCall.callId = "not " + oldMockCall.callId;
@ -985,6 +965,7 @@ describe('Group Call', function() {
expect(oldMockCall.hangup).toHaveBeenCalled();
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
// @ts-ignore
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, newMockCall]]),
@ -995,7 +976,7 @@ describe('Group Call', function() {
// First we leave the call since we have already entered
groupCall.leave();
const call = new MockCall(room.roomId, groupCall.groupCallId);
const call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockClient.callEventHandler!.calls = new Map<string, MatrixCall>([
[call.callId, call.typed()],
]);
@ -1068,7 +1049,10 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
// @ts-ignore
const call = groupCall.calls
.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!
.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
call.onNegotiateReceived({
getContent: () => ({
@ -1270,4 +1254,155 @@ describe('Group Call', function() {
});
});
});
describe("cleaning member state", () => {
const bobWeb: IMyDevice = {
device_id: "bobweb",
last_seen_ts: 0,
};
const bobDesktop: IMyDevice = {
device_id: "bobdesktop",
last_seen_ts: 0,
};
const bobDesktopOffline: IMyDevice = {
device_id: "bobdesktopoffline",
last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago
};
const bobDesktopNeverOnline: IMyDevice = {
device_id: "bobdesktopneveronline",
};
const mkContent = (devices: IMyDevice[]) => ({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10,
})),
}],
});
const expectDevices = (devices: IMyDevice[]) => expect(
room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)?.getContent(),
).toEqual({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number),
})),
}],
});
let mockClient: MatrixClient;
let room: Room;
let groupCall: GroupCall;
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => jest.useRealTimers());
beforeEach(async () => {
const typedMockClient = new MockCallMatrixClient(
FAKE_USER_ID_2, bobWeb.device_id, FAKE_SESSION_ID_2,
);
jest.spyOn(typedMockClient, "sendStateEvent").mockImplementation(
async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`;
if (roomId === room.roomId) {
room.addLiveEvents([new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
})]);
}
return { event_id: eventId };
},
);
mockClient = typedMockClient as unknown as MatrixClient;
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
groupCall = new GroupCall(
mockClient,
room,
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
FAKE_CONF_ID,
);
await groupCall.create();
mockClient.getDevices = async () => ({
devices: [
bobWeb,
bobDesktop,
bobDesktopOffline,
bobDesktopNeverOnline,
],
});
});
afterEach(() => groupCall.leave());
it("doesn't clean up valid devices", async () => {
await groupCall.enter();
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobWeb, bobDesktop]);
});
it("cleans up our own device if we're disconnected", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});
it("doesn't clean up the local device if entered via another session", async () => {
groupCall.enteredViaAnotherSession = true;
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobWeb]);
});
it("cleans up devices that have never been online", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobDesktop, bobDesktopNeverOnline]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});
it("no-ops if there are no state events", async () => {
await groupCall.cleanMemberState();
expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null);
});
});
});

View File

@ -18,7 +18,7 @@ export interface IIdentityServerProvider {
/**
* Gets an access token for use against the identity server,
* for the associated client.
* @returns {Promise<string>} Resolves to the access token.
* @returns Promise which resolves to the access token.
*/
getAccessToken(): Promise<string>;
}

View File

@ -35,7 +35,8 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
* To achieve an arbitrary number of only owner-writable state events
* we introduce a variable suffix to the event type
*
* Eg
* @example
* ```
* {
* "type": "m.beacon_info.@matthew:matrix.org.1",
* "state_key": "@matthew:matrix.org",
@ -58,6 +59,7 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
* // more content as described below
* }
* }
* ```
*/
/**
@ -78,20 +80,23 @@ export type MBeaconInfoContent = {
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* @example
* ```
* {
"type": "m.beacon_info",
"state_key": "@matthew:matrix.org",
"content": {
"m.beacon_info": {
"description": "The Matthew Tracker", // same as an `m.location` description
"timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
},
"m.ts": 1436829458432, // creation timestamp of the beacon on the client
"m.asset": {
"type": "m.self" // the type of asset being tracked as per MSC3488
}
}
}
* "type": "m.beacon_info",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "The Matthew Tracker", // same as an `m.location` description
* "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
* },
* "m.ts": 1436829458432, // creation timestamp of the beacon on the client
* "m.asset": {
* "type": "m.self" // the type of asset being tracked as per MSC3488
* }
* }
* }
* ```
*/
/**
@ -107,22 +112,24 @@ export type MBeaconInfoEventContent = &
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*
* @example
* ```
* {
"type": "m.beacon",
"sender": "@matthew:matrix.org",
"content": {
"m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
"rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
"event_id": "$beacon_info"
},
"m.location": {
"uri": "geo:51.5008,0.1247;u=35",
"description": "Arbitrary beacon information"
},
"m.ts": 1636829458432,
}
}
* "type": "m.beacon",
* "sender": "@matthew:matrix.org",
* "content": {
* "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
* "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
* "event_id": "$beacon_info"
* },
* "m.location": {
* "uri": "geo:51.5008,0.1247;u=35",
* "description": "Arbitrary beacon information"
* },
* "m.ts": 1636829458432,
* }
* }
* ```
*/
/**

View File

@ -120,6 +120,8 @@ export enum RoomType {
ElementVideo = "io.element.video",
}
export const ToDeviceMessageId = "org.matrix.msgid";
/**
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
@ -169,17 +171,21 @@ export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.m
* eventual removal.
*
* Schema (TypeScript):
* ```
* {
* service_members?: string[]
* }
* ```
*
* Example:
* @example
* ```
* {
* "service_members": [
* "@helperbot:localhost",
* "@reminderbot:alice.tdl"
* ]
* }
* ```
*/
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",

View File

@ -42,7 +42,7 @@ export type ReceiptCache = {[eventId: string]: CachedReceipt[]};
export interface ReceiptContent {
[eventId: string]: {
[key in ReceiptType]: {
[key in ReceiptType | string]: {
[userId: string]: Receipt;
};
};

View File

@ -54,17 +54,31 @@ export interface ISendEventResponse {
}
export interface IPresenceOpts {
// One of "online", "offline" or "unavailable"
presence: "online" | "offline" | "unavailable";
// The status message to attach.
status_msg?: string;
}
export interface IPaginateOpts {
// true to fill backwards, false to go forwards
backwards?: boolean;
// number of events to request
limit?: number;
}
export interface IGuestAccessOpts {
/**
* True to allow guests to join this room. This
* implicitly gives guests write access. If false or not given, guests are
* explicitly forbidden from joining the room.
*/
allowJoin: boolean;
/**
* True to set history visibility to
* be world_readable. This gives guests read access *from this point forward*.
* If false or not given, history visibility is not modified.
*/
allowRead: boolean;
}
@ -74,7 +88,9 @@ export interface ISearchOpts {
}
export interface IEventSearchOpts {
// a JSON filter object to pass in the request
filter?: IRoomEventFilter;
// the term to search for
term: string;
}
@ -92,9 +108,13 @@ export interface ICreateRoomStateEvent {
}
export interface ICreateRoomOpts {
// The alias localpart to assign to this room.
room_alias_name?: string;
// Either 'public' or 'private'.
visibility?: Visibility;
// The name to give this room.
name?: string;
// The topic to give this room.
topic?: string;
preset?: Preset;
power_level_content_override?: {
@ -111,6 +131,7 @@ export interface ICreateRoomOpts {
};
creation_content?: object;
initial_state?: ICreateRoomStateEvent[];
// A list of user IDs to invite to this room.
invite?: string[];
invite_3pid?: IInvite3PID[];
is_direct?: boolean;
@ -121,7 +142,10 @@ export interface IRoomDirectoryOptions {
server?: string;
limit?: number;
since?: string;
// Filter parameters
filter?: {
// String to search for
generic_search_term?: string;
room_types?: Array<RoomType | null>;
};
@ -153,7 +177,6 @@ export interface IRelationsRequestOpts {
}
export interface IRelationsResponse {
original_event: IEvent;
chunk: IEvent[];
next_batch?: string;
prev_batch?: string;

View File

@ -21,10 +21,9 @@ import { UnstableValue } from "../NamespacedValue";
/**
* Extensible topic event type based on MSC3765
* https://github.com/matrix-org/matrix-spec-proposals/pull/3765
*/
/**
* Eg
*
* @example
* ```
* {
* "type": "m.room.topic,
* "state_key": "",
@ -39,6 +38,7 @@ import { UnstableValue } from "../NamespacedValue";
* }],
* }
* }
* ```
*/
/**

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ToDeviceMessageId } from './@types/event';
import { logger } from "./logger";
import { MatrixError, MatrixClient } from "./matrix";
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
@ -54,12 +55,15 @@ export class ToDeviceMessageQueue {
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}`);
const msgmap = batchWithTxnId.batch.map(
(msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`,
);
logger.info(
`Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`, msgmap,
);
}
await this.client.store.saveToDeviceBatches(batches);
logger.info(`Enqueued to-device messages with txn ids ${batches.map((batch) => batch.txnId)}`);
this.sendQueue();
}

View File

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/** @module auto-discovery */
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { MatrixError, Method, timeoutSignal } from "./http-api";
@ -49,7 +47,7 @@ interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
error?: IWellKnownConfig["error"] | null;
}
interface ClientConfig {
interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
"m.homeserver": WellKnownConfig;
"m.identity_server": WellKnownConfig;
}
@ -87,8 +85,6 @@ export class AutoDiscovery {
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
* @return {string}
* @constructor
*/
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
@ -98,8 +94,6 @@ export class AutoDiscovery {
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
* @return {string}
* @constructor
*/
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
@ -107,15 +101,11 @@ export class AutoDiscovery {
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
* @return {string}
* @constructor
*/
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
@ -125,13 +115,13 @@ export class AutoDiscovery {
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {object} wellknown The configuration object itself, as returned
* @param wellknown - The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* @returns Promise which resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
public static async fromDiscoveryConfig(wellknown: any): Promise<ClientConfig> {
public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
@ -185,7 +175,7 @@ export class AutoDiscovery {
const hsVersions = await this.fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
if (!hsVersions || !hsVersions.raw?.["versions"]) {
logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
@ -219,9 +209,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this.sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error =
@ -234,7 +222,7 @@ export class AutoDiscovery {
const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@ -259,14 +247,16 @@ export class AutoDiscovery {
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown).forEach((k) => {
Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
for (const prop of Object.keys(wellknown[k]!)) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
type Prop = Exclude<keyof IWellKnownConfig, "error" | "state" | "base_url">;
// @ts-ignore - ts gets unhappy as we're mixing types here
clientConfig[k][prop as Prop] = wellknown[k]![prop as Prop];
}
} else {
// Just copy the whole thing over otherwise
@ -284,9 +274,9 @@ export class AutoDiscovery {
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param {string} domain The homeserver domain to perform discovery
* @param domain - The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
* @returns Promise which resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
@ -347,15 +337,15 @@ export class AutoDiscovery {
}
// Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!);
}
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param {string} domain The domain to get the client config for.
* @returns {Promise<object>} Resolves to the domain's client config. Can
* @param domain - The domain to get the client config for.
* @returns Promise which resolves to the domain's client config. Can
* be an empty object.
*/
public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
@ -374,11 +364,11 @@ export class AutoDiscovery {
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param {string} url The potentially invalid URL to sanitize.
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
* @param url - The potentially invalid URL to sanitize.
* @returns The sanitized URL or a falsey value if the URL is invalid.
* @internal
*/
private static sanitizeWellKnownUrl(url: string): string | false {
private static sanitizeWellKnownUrl(url?: string | null): string | false {
if (!url) return false;
try {
@ -430,9 +420,9 @@ export class AutoDiscovery {
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human-readable description of what went wrong.
* error: The actual Error, if one exists.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
* @param url - The URL to fetch a JSON object from.
* @returns Promise which resolves to the returned state.
* @internal
*/
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
let response: Response;

View File

@ -16,27 +16,28 @@ limitations under the License.
import * as matrixcs from "./matrix";
type BrowserMatrix = typeof matrixcs;
declare global {
/* eslint-disable no-var, camelcase */
var __js_sdk_entrypoint: boolean;
var matrixcs: BrowserMatrix;
/* eslint-enable no-var */
}
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
global.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
let indexedDB;
// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
let indexedDB: IDBFactory | undefined;
try {
indexedDB = global.indexedDB;
} catch (e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto",
);
},
);
matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
}
// We export 3 things to make browserify happy as well as downstream projects.

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/** @module ContentHelpers */
import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
@ -37,9 +35,9 @@ import { IContent } from "./models/event";
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
* @param body - the plaintext body of the message
* @param htmlBody - the HTML representation of the message
* @returns
*/
export function makeHtmlMessage(body: string, htmlBody: string): IContent {
return {
@ -52,9 +50,9 @@ export function makeHtmlMessage(body: string, htmlBody: string): IContent {
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
* @param body - the plaintext body of the notice
* @param htmlBody - the HTML representation of the notice
* @returns
*/
export function makeHtmlNotice(body: string, htmlBody: string): IContent {
return {
@ -67,9 +65,9 @@ export function makeHtmlNotice(body: string, htmlBody: string): IContent {
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
* @param body - the plaintext body of the emote
* @param htmlBody - the HTML representation of the emote
* @returns
*/
export function makeHtmlEmote(body: string, htmlBody: string): IContent {
return {
@ -82,8 +80,8 @@ export function makeHtmlEmote(body: string, htmlBody: string): IContent {
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
* @param body - the plaintext body of the emote
* @returns
*/
export function makeTextMessage(body: string): IContent {
return {
@ -94,8 +92,8 @@ export function makeTextMessage(body: string): IContent {
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
* @param body - the plaintext body of the notice
* @returns
*/
export function makeNotice(body: string): IContent {
return {
@ -106,8 +104,8 @@ export function makeNotice(body: string): IContent {
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
* @param body - the plaintext body of the emote
* @returns
*/
export function makeEmoteMessage(body: string): IContent {
return {
@ -139,11 +137,11 @@ 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 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
* @param uri - a geo:// uri for the location
* @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
*/
export const makeLocationContent = (
// this is first but optional

View File

@ -13,25 +13,22 @@ 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.
*/
/**
* @module content-repo
*/
import * as utils from "./utils";
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* @param baseUrl - The base homeserver url which has a content repo.
* @param mxc - The mxc:// URI.
* @param width - The desired width of the thumbnail.
* @param height - The desired height of the thumbnail.
* @param resizeMethod - The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* @param allowDirectLinks - If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
* @returns The complete URL to the content.
*/
export function getHttpUriForMxc(
baseUrl: string,

View File

@ -16,7 +16,6 @@ limitations under the License.
/**
* Cross signing methods
* @module crypto/CrossSigning
*/
import { PkSigning } from "@matrix-org/olm";
@ -67,12 +66,10 @@ export class CrossSigningInfo {
/**
* Information about a user's cross-signing keys
*
* @class
*
* @param {string} userId the user that the information is about
* @param {object} callbacks Callbacks used to interact with the app
* @param userId - the user that the information is about
* @param callbacks - Callbacks used to interact with the app
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
* @param cacheCallbacks - Callbacks used to interact with the cache
*/
public constructor(
public readonly userId: string,
@ -84,6 +81,7 @@ export class CrossSigningInfo {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
// @ts-ignore - ts doesn't like this and nor should we
res[prop] = obj[prop];
}
}
@ -101,10 +99,10 @@ export class CrossSigningInfo {
/**
* Calls the app callback to ask for a private key
*
* @param {string} type The key type ("master", "self_signing", or "user_signing")
* @param {string} expectedPubkey The matching public key or undefined to use
* @param type - The key type ("master", "self_signing", or "user_signing")
* @param expectedPubkey - The matching public key or undefined to use
* the stored public key for the given key type.
* @returns {Array} An array with [ public key, Olm.PkSigning ]
* @returns An array with [ public key, Olm.PkSigning ]
*/
public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
@ -164,8 +162,8 @@ export class CrossSigningInfo {
* XXX: This could be static, be we often seem to have an instance when we
* want to know this anyway...
*
* @param {SecretStorage} secretStorage The secret store using account data
* @returns {object} map of key name to key info the secret is encrypted
* @param secretStorage - The secret store using account data
* @returns map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
@ -193,8 +191,8 @@ export class CrossSigningInfo {
* typically called in conjunction with the creation of new cross-signing
* keys.
*
* @param {Map} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data
* @param keys - The keys to store
* @param secretStorage - The secret store using account data
*/
public static async storeInSecretStorage(
keys: Map<string, Uint8Array>,
@ -210,10 +208,10 @@ export class CrossSigningInfo {
* Get private keys from secret storage created by some other device. This
* also passes the private keys to the app-specific callback.
*
* @param {string} type The type of key to get. One of "master",
* @param type - The type of key to get. One of "master",
* "self_signing", or "user_signing".
* @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key
* @param secretStorage - The secret store using account data
* @returns The private key
*/
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
@ -226,9 +224,9 @@ export class CrossSigningInfo {
/**
* Check whether the private keys exist in the local key cache.
*
* @param {string} [type] The type of key to get. One of "master",
* @param type - The type of key to get. One of "master",
* "self_signing", or "user_signing". Optional, will check all by default.
* @returns {boolean} True if all keys are stored in the local cache.
* @returns True if all keys are stored in the local cache.
*/
public async isStoredInKeyCache(type?: string): Promise<boolean> {
const cacheCallbacks = this.cacheCallbacks;
@ -245,7 +243,7 @@ export class CrossSigningInfo {
/**
* Get cross-signing private keys from the local cache.
*
* @returns {Map} A map from key type (string) to private key (Uint8Array)
* @returns A map from key type (string) to private key (Uint8Array)
*/
public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
const keys = new Map();
@ -265,10 +263,10 @@ export class CrossSigningInfo {
* Get the ID used to identify the user. This can also be used to test for
* the existence of a given key type.
*
* @param {string} type The type of key to get the ID of. One of "master",
* @param type - The type of key to get the ID of. One of "master",
* "self_signing", or "user_signing". Defaults to "master".
*
* @return {string} the ID
* @returns the ID
*/
public getId(type = "master"): string | null {
if (!this.keys[type]) return null;
@ -281,7 +279,7 @@ export class CrossSigningInfo {
* will be held in this class, while the private keys are passed off to the
* `saveCrossSigningKeys` application callback.
*
* @param {CrossSigningLevel} level The key types to reset
* @param level - The key types to reset
*/
public async resetKeys(level?: CrossSigningLevel): Promise<void> {
if (!this.callbacks.saveCrossSigningKeys) {
@ -501,9 +499,9 @@ export class CrossSigningInfo {
/**
* Check whether a given user is trusted.
*
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param userCrossSigning - Cross signing info for user
*
* @returns {UserTrustLevel}
* @returns
*/
public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
// if we're checking our own key, then it's trusted if the master key
@ -541,12 +539,12 @@ export class CrossSigningInfo {
/**
* Check whether a given device is trusted.
*
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param {module:crypto/deviceinfo} device The device to check
* @param {boolean} localTrust Whether the device is trusted locally
* @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices
* @param userCrossSigning - Cross signing info for user
* @param device - The device to check
* @param localTrust - Whether the device is trusted locally
* @param trustCrossSignedDevices - Whether we trust cross signed devices
*
* @returns {DeviceTrustLevel}
* @returns
*/
public checkDeviceTrust(
userCrossSigning: CrossSigningInfo,
@ -579,7 +577,7 @@ export class CrossSigningInfo {
}
/**
* @returns {object} Cache callbacks
* @returns Cache callbacks
*/
public getCacheCallbacks(): ICacheCallbacks {
return this.cacheCallbacks;
@ -620,21 +618,21 @@ export class UserTrustLevel {
) {}
/**
* @returns {boolean} true if this user is verified via any means
* @returns true if this user is verified via any means
*/
public isVerified(): boolean {
return this.isCrossSigningVerified();
}
/**
* @returns {boolean} true if this user is verified via cross signing
* @returns true if this user is verified via cross signing
*/
public isCrossSigningVerified(): boolean {
return this.crossSigningVerified;
}
/**
* @returns {boolean} true if we ever verified this user before (at least for
* @returns true if we ever verified this user before (at least for
* the history of verifications observed by this device).
*/
public wasCrossSigningVerified(): boolean {
@ -642,7 +640,7 @@ export class UserTrustLevel {
}
/**
* @returns {boolean} true if this user's key is trusted on first use
* @returns true if this user's key is trusted on first use
*/
public isTofu(): boolean {
return this.tofu;
@ -674,7 +672,7 @@ export class DeviceTrustLevel {
}
/**
* @returns {boolean} true if this device is verified via any means
* @returns true if this device is verified via any means
*/
public isVerified(): boolean {
return Boolean(this.isLocallyVerified() || (
@ -683,21 +681,21 @@ export class DeviceTrustLevel {
}
/**
* @returns {boolean} true if this device is verified via cross signing
* @returns true if this device is verified via cross signing
*/
public isCrossSigningVerified(): boolean {
return this.crossSigningVerified;
}
/**
* @returns {boolean} true if this device is verified locally
* @returns true if this device is verified locally
*/
public isLocallyVerified(): boolean {
return this.localVerified;
}
/**
* @returns {boolean} true if this device is trusted from a user's key
* @returns true if this device is trusted from a user's key
* that is trusted on first use
*/
public isTofu(): boolean {
@ -756,9 +754,9 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning],
/**
* Request cross-signing keys from another device during verification.
*
* @param {MatrixClient} baseApis base Matrix API interface
* @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified
* @param baseApis - base Matrix API interface
* @param userId - The user ID being verified
* @param deviceId - The device ID being verified
*/
export async function requestKeysDuringVerification(
baseApis: MatrixClient,

View File

@ -15,8 +15,6 @@ limitations under the License.
*/
/**
* @module crypto/DeviceList
*
* Manages the list of other users' devices
*/
@ -64,9 +62,6 @@ export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
/**
* @alias module:crypto/DeviceList
*/
export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
@ -163,11 +158,11 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* The actual save will be delayed by a short amount of time to
* aggregate multiple writes to the database.
*
* @param {number} delay Time in ms before which the save actually happens.
* @param delay - Time in ms before which the save actually happens.
* By default, the save is delayed for a short period in order to batch
* multiple writes, but this behaviour can be disabled by passing 0.
*
* @return {Promise<boolean>} true if the data was saved, false if
* @returns true if the data was saved, false if
* it was not (eg. because no changes were pending). The promise
* will only resolve once the data is saved, so may take some time
* to resolve.
@ -236,7 +231,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Gets the sync token last set with setSyncToken
*
* @return {string} The sync token
* @returns The sync token
*/
public getSyncToken(): string | null {
return this.syncToken;
@ -250,7 +245,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* those changed will not be synced from the server if a new client starts
* up with that data.
*
* @param {string} st The sync token
* @param st - The sync token
*/
public setSyncToken(st: string | null): void {
this.syncToken = st;
@ -260,11 +255,10 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* Ensures up to date keys for a list of users are stored in the session store,
* downloading and storing them if they're not (or if forceDownload is
* true).
* @param {Array} userIds The users to fetch.
* @param {boolean} forceDownload Always download the keys even if cached.
* @param userIds - The users to fetch.
* @param forceDownload - Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}.
* @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}.
*/
public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
const usersToDownload: string[] = [];
@ -303,9 +297,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Get the stored device keys for a list of user ids
*
* @param {string[]} userIds the list of users to list keys for.
* @param userIds - the list of users to list keys for.
*
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
* @returns userId-\>deviceId-\>{@link DeviceInfo}.
*/
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
const stored: DeviceInfoMap = {};
@ -322,7 +316,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Returns a list of all user IDs the DeviceList knows about
*
* @return {array} All known user IDs
* @returns All known user IDs
*/
public getKnownUserIds(): string[] {
return Object.keys(this.devices);
@ -331,9 +325,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
* @param userId - the user to list keys for.
*
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
* @returns list of devices, or null if we haven't
* managed to get a list of devices for this user yet.
*/
public getStoredDevicesForUser(userId: string): DeviceInfo[] | null {
@ -353,9 +347,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Get the stored device data for a user, in raw object form
*
* @param {string} userId the user to get data for
* @param userId - the user to get data for
*
* @return {Object} deviceId->{object} devices, or undefined if
* @returns `deviceId->{object}` devices, or undefined if
* there is no data for this user.
*/
public getRawStoredDevicesForUser(userId: string): Record<string, IDevice> {
@ -376,10 +370,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} device, or undefined
* @returns device, or undefined
* if we don't know about this device
*/
public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
@ -393,10 +385,10 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Get a user ID by one of their device's curve25519 identity key
*
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
* @param algorithm - encryption algorithm
* @param senderKey - curve25519 key to match
*
* @return {string} user ID
* @returns user ID
*/
public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
@ -410,10 +402,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Find a device by curve25519 identity key
*
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
* @param algorithm - encryption algorithm
* @param senderKey - curve25519 key to match
*/
public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null {
const userId = this.getUserByIdentityKey(algorithm, senderKey);
@ -453,8 +443,8 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Replaces the list of devices for a user with the given device list
*
* @param {string} userId The user ID
* @param {Object} devices New device info for user
* @param userId - The user ID
* @param devices - New device info for user
*/
public storeDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
this.setRawStoredDevicesForUser(userId, devices);
@ -468,7 +458,6 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* will download the device list for the user, and that subsequent calls to
* invalidateUserDeviceList will trigger more updates.
*
* @param {String} userId
*/
public startTrackingDeviceList(userId: string): void {
// sanity-check the userId. This is mostly paranoia, but if synapse
@ -497,7 +486,6 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* complete; it will just mean that we don't think that we have an up-to-date
* list for future calls to downloadKeys.
*
* @param {String} userId
*/
public stopTrackingDeviceList(userId: string): void {
if (this.deviceTrackingStatus[userId]) {
@ -532,7 +520,6 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* This doesn't actually set off an update, so that several users can be
* batched together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
public invalidateUserDeviceList(userId: string): void {
if (this.deviceTrackingStatus[userId]) {
@ -548,7 +535,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* If we have users who have outdated device lists, start key downloads for them
*
* @returns {Promise} which completes when the download completes; normally there
* @returns which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests).
*/
public refreshOutdatedDeviceLists(): Promise<void> {
@ -569,9 +556,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* Set the stored device data for a user, in raw object form
* Used only by internal class DeviceListUpdateSerialiser
*
* @param {string} userId the user to get data for
* @param userId - the user to get data for
*
* @param {Object} devices deviceId->{object} the new devices
* @param devices - `deviceId->{object}` the new devices
*/
public setRawStoredDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
// remove old devices from userByIdentityKey
@ -602,9 +589,9 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* device list tracking status for them, and the
* keyDownloadsInProgressByUser map for them.
*
* @param {String[]} users list of userIds
* @param users - list of userIds
*
* @return {Promise} resolves when all the users listed have
* @returns resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
@ -689,9 +676,9 @@ class DeviceListUpdateSerialiser {
private syncToken?: string; // The sync token we send with the requests
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object, the device list to be updated
* @param baseApis - Base API object
* @param olmDevice - The Olm Device
* @param deviceList - The device list object, the device list to be updated
*/
public constructor(
private readonly baseApis: MatrixClient,
@ -702,12 +689,12 @@ class DeviceListUpdateSerialiser {
/**
* Make a key query request for the given users
*
* @param {String[]} users list of user ids
* @param users - list of user ids
*
* @param {String} syncToken sync token to pass in the query request, to
* @param syncToken - sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {Promise} resolves when all the users listed have
* @returns resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/

View File

@ -35,7 +35,7 @@ import { IAccountDataClient } from "./SecretStorage";
interface ICrossSigningKeys {
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
keys: Record<string, ICrossSigningKey>;
keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
}
/**
@ -58,8 +58,8 @@ export class EncryptionSetupBuilder {
private sessionBackupPrivateKey?: Uint8Array;
/**
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
* @param accountData - pre-existing account data, will only be read, not written.
* @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet
*/
public constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
@ -70,13 +70,13 @@ export class EncryptionSetupBuilder {
/**
* Adds new cross-signing public keys
*
* @param {function} authUpload Function called to await an interactive auth
* @param authUpload - Function called to await an interactive auth
* flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives
* A function that makes the request requiring auth. Receives
* the auth data as an object. Can be called multiple times, first with
* an empty authDict, to obtain the flows.
* @param {Object} keys the new keys
* @param keys - the new keys
*/
public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void {
this.crossSigningKeys = { authUpload, keys };
@ -88,7 +88,7 @@ export class EncryptionSetupBuilder {
* Used either to create a new key backup, or add signatures
* from the new MSK.
*
* @param {Object} keyBackupInfo as received from/sent to the server
* @param keyBackupInfo - as received from/sent to the server
*/
public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void {
this.keyBackupInfo = keyBackupInfo;
@ -99,7 +99,6 @@ export class EncryptionSetupBuilder {
*
* Used after fixing the format of the key
*
* @param {Uint8Array} privateKey
*/
public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void {
this.sessionBackupPrivateKey = privateKey;
@ -109,9 +108,6 @@ export class EncryptionSetupBuilder {
* Add signatures from a given user and device/x-sign key
* Used to sign the new cross-signing key with the device key
*
* @param {String} userId
* @param {String} deviceId
* @param {Object} signature
*/
public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void {
if (!this.keySignatures) {
@ -122,18 +118,12 @@ export class EncryptionSetupBuilder {
userSignatures[deviceId] = signature;
}
/**
* @param {String} type
* @param {Object} content
* @return {Promise}
*/
public async setAccountData(type: string, content: object): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
/**
* builds the operation containing all the parts that have been added to the builder
* @return {EncryptionSetupOperation}
*/
public buildOperation(): EncryptionSetupOperation {
const accountData = this.accountDataClientAdapter.values;
@ -150,9 +140,6 @@ export class EncryptionSetupBuilder {
*
* This does not yet store the operation in a way that it can be restored,
* but that is the idea in the future.
*
* @param {Crypto} crypto
* @return {Promise}
*/
public async persist(crypto: Crypto): Promise<void> {
// store private keys in cache
@ -187,10 +174,6 @@ export class EncryptionSetupBuilder {
*/
export class EncryptionSetupOperation {
/**
* @param {Map<String, Object>} accountData
* @param {Object} crossSigningKeys
* @param {Object} keyBackupInfo
* @param {Object} keySignatures
*/
public constructor(
private readonly accountData: Map<string, object>,
@ -201,7 +184,6 @@ export class EncryptionSetupOperation {
/**
* Runs the (remaining part of, in the future) operation by sending requests to the server.
* @param {Crypto} crypto
*/
public async apply(crypto: Crypto): Promise<void> {
const baseApis = crypto.baseApis;
@ -209,7 +191,7 @@ export class EncryptionSetupOperation {
if (this.crossSigningKeys) {
const keys: Partial<CrossSigningKeys> = {};
for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
keys[name + "_key"] = key;
keys[((name as keyof ICrossSigningKeys["keys"]) + "_key" as keyof CrossSigningKeys)] = key;
}
// We must only call `uploadDeviceSigningKeys` from inside this auth
@ -270,23 +252,21 @@ class AccountDataClientAdapter
public readonly values = new Map<string, MatrixEvent>();
/**
* @param {Object.<String, MatrixEvent>} existingValues existing account data
* @param existingValues - existing account data
*/
public constructor(private readonly existingValues: Record<string, MatrixEvent>) {
super();
}
/**
* @param {String} type
* @return {Promise<Object>} the content of the account data
* @returns the content of the account data
*/
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
* @returns the content of the account data
*/
public getAccountData(type: string): IContent | null {
const modifiedValue = this.values.get(type);
@ -300,11 +280,6 @@ class AccountDataClientAdapter
return null;
}
/**
* @param {String} type
* @param {Object} content
* @return {Promise}
*/
public setAccountData(type: string, content: any): Promise<{}> {
const lastEvent = this.values.get(type);
this.values.set(type, content);

View File

@ -23,11 +23,19 @@ import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData } from "./index";
import { OlmGroupSessionExtraData } from "../@types/crypto";
import { IMessage } from "./algorithms/olm";
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
export class PayloadTooLargeError extends Error {
public readonly data = {
errcode: "M_TOO_LARGE",
error: "Payload too large for encrypted message",
};
}
function checkPayloadLength(payloadString: string): void {
if (payloadString === undefined) {
throw new Error("payloadString undefined");
@ -40,55 +48,27 @@ function checkPayloadLength(payloadString: string): void {
// Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case.
const err = new Error("Message too long (" + payloadString.length + " bytes). " +
"The maximum for an encrypted message is " +
MAX_PLAINTEXT_LENGTH + " bytes.");
// TODO: [TypeScript] We should have our own error types
err["data"] = {
errcode: "M_TOO_LARGE",
error: "Payload too large for encrypted message",
};
throw err;
throw new PayloadTooLargeError(`Message too long (${payloadString.length} bytes). ` +
`The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`);
}
}
/**
* The type of object we use for importing and exporting megolm session data.
*
* @typedef {Object} module:crypto/OlmDevice.MegolmSessionData
* @property {String} sender_key Sender's Curve25519 device key
* @property {String[]} forwarding_curve25519_key_chain Devices which forwarded
* this session to us (normally empty).
* @property {Object<string, string>} sender_claimed_keys Other keys the sender claims.
* @property {String} room_id Room this session is used in
* @property {String} session_id Unique id for the session
* @property {String} session_key Base64'ed key data
*/
interface IInitOpts {
fromExportedDevice?: IExportedDevice;
pickleKey?: string;
}
/**
* data stored in the session store about an inbound group session
*
* @typedef {Object} InboundGroupSessionData
* @property {string} room_id
* @property {string} session pickled Olm.InboundGroupSession
* @property {Object<string, string>} keysClaimed
* @property {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding
* this session to us (normally empty).
* @property {boolean=} untrusted whether this session is untrusted.
* @property {boolean=} sharedHistory whether this session exists during the room being set to shared history.
*/
/** data stored in the session store about an inbound group session */
export interface InboundGroupSessionData {
room_id: string; // eslint-disable-line camelcase
/** pickled Olm.InboundGroupSession */
session: string;
keysClaimed: Record<string, string>;
/** Devices involved in forwarding this session to us (normally empty). */
forwardingCurve25519KeyChain: string[];
/** whether this session is untrusted. */
untrusted?: boolean;
/** whether this session exists during the room being set to shared history. */
sharedHistory?: boolean;
}
@ -126,25 +106,20 @@ interface IInboundGroupSessionKey {
}
/* eslint-enable camelcase */
type OneTimeKeys = { curve25519: { [keyId: string]: string } };
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in the cryptoStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} cryptoStore A store for crypto data
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
export class OlmDevice {
public pickleKey = "DEFAULT_KEY"; // set by consumers
// don't know these until we load the account from storage in init()
/** Curve25519 key for the account, unknown until we load the account from storage in init() */
public deviceCurve25519Key: string | null = null;
/** Ed25519 key for the account, unknown until we load the account from storage in init() */
public deviceEd25519Key: string | null = null;
private maxOneTimeKeys: number | null = null;
@ -181,7 +156,7 @@ export class OlmDevice {
}
/**
* @return {array} The version of Olm.
* @returns The version of Olm.
*/
public static getOlmVersion(): [number, number, number] {
return global.Olm.get_library_version();
@ -199,12 +174,11 @@ export class OlmDevice {
*
* Reads the device keys from the OlmAccount object.
*
* @param {object} opts
* @param {object} opts.fromExportedDevice (Optional) data from exported device
* @param fromExportedDevice - (Optional) data from exported device
* that must be re-created.
* If present, opts.pickleKey is ignored
* (exported data already provides a pickle key)
* @param {object} opts.pickleKey (Optional) pickle key to set instead of default one
* @param pickleKey - (Optional) pickle key to set instead of default one
*/
public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise<void> {
let e2eKeys;
@ -242,9 +216,9 @@ export class OlmDevice {
* Note that for now only the “account” and “sessions” stores are populated;
* Other stores will be as with a new device.
*
* @param {IExportedDevice} exportedData Data exported from another device
* @param exportedData - Data exported from another device
* through the “export” method.
* @param {Olm.Account} account an olm account to initialize
* @param account - an olm account to initialize
*/
private async initialiseFromExportedDevice(exportedData: IExportedDevice, account: Account): Promise<void> {
await this.cryptoStore.doTxn(
@ -302,9 +276,8 @@ export class OlmDevice {
* This function requires a live transaction object from cryptoStore.doTxn()
* and therefore may only be called in a doTxn() callback.
*
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {function} func
* @private
* @param txn - Opaque transaction object from cryptoStore.doTxn()
* @internal
*/
private getAccount(txn: unknown, func: (account: Account) => void): void {
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
@ -323,9 +296,9 @@ export class OlmDevice {
* This function requires a live transaction object from cryptoStore.doTxn()
* and therefore may only be called in a doTxn() callback.
*
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {object} Olm.Account object
* @private
* @param txn - Opaque transaction object from cryptoStore.doTxn()
* @param Olm.Account object
* @internal
*/
private storeAccount(txn: unknown, account: Account): void {
this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey));
@ -335,7 +308,7 @@ export class OlmDevice {
* Export data for re-creating the Olm device later.
* TODO export data other than just account and (P2P) sessions.
*
* @return {Promise<object>} The exported data
* @returns The exported data
*/
public async export(): Promise<IExportedDevice> {
const result: Partial<IExportedDevice> = {
@ -370,11 +343,8 @@ export class OlmDevice {
* function and will be freed as soon the callback returns. It is *not*
* usable for the rest of the lifetime of the transaction.
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {function} func
* @private
* @param txn - Opaque transaction object from cryptoStore.doTxn()
* @internal
*/
private getSession(
deviceKey: string,
@ -394,9 +364,7 @@ export class OlmDevice {
* function with it. The session object is destroyed once the function
* returns.
*
* @param {object} sessionInfo
* @param {function} func
* @private
* @internal
*/
private unpickleSession(
sessionInfo: ISessionInfo,
@ -416,10 +384,9 @@ export class OlmDevice {
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int}
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @private
* @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}`
* @param txn - Opaque transaction object from cryptoStore.doTxn()
* @internal
*/
private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void {
const sessionId = sessionInfo.session.session_id();
@ -432,9 +399,8 @@ export class OlmDevice {
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
* @returns result of func
* @internal
*/
private getUtility<T>(func: (utility: Utility) => T): T {
const utility = new global.Olm.Utility();
@ -448,11 +414,11 @@ export class OlmDevice {
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {Promise<string>} base64-encoded signature
* @param message - message to be signed
* @returns base64-encoded signature
*/
public async sign(message: string): Promise<string> {
let result;
let result: string;
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
@ -460,18 +426,18 @@ export class OlmDevice {
result = account.sign(message);
});
});
return result;
return result!;
}
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* @returns one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
public async getOneTimeKeys(): Promise<{ curve25519: { [keyId: string]: string } }> {
let result;
public async getOneTimeKeys(): Promise<OneTimeKeys> {
let result: OneTimeKeys;
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
@ -481,13 +447,13 @@ export class OlmDevice {
},
);
return result;
return result!;
}
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
* @returns number of keys
*/
public maxNumberOfOneTimeKeys(): number {
return this.maxOneTimeKeys ?? -1;
@ -511,8 +477,8 @@ export class OlmDevice {
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
* @return {Promise} Resolved once the account is saved back having generated the keys
* @param numKeys - number of keys to generate
* @returns Resolved once the account is saved back having generated the keys
*/
public generateOneTimeKeys(numKeys: number): Promise<void> {
return this.cryptoStore.doTxn(
@ -529,7 +495,7 @@ export class OlmDevice {
/**
* Generate a new fallback keys
*
* @return {Promise} Resolved once the account is saved back having generated the key
* @returns Resolved once the account is saved back having generated the key
*/
public async generateFallbackKey(): Promise<void> {
await this.cryptoStore.doTxn(
@ -573,9 +539,9 @@ export class OlmDevice {
*
* The new session will be stored in the cryptoStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
* @param theirIdentityKey - remote user's Curve25519 identity key
* @param theirOneTimeKey - remote user's one-time Curve25519 key
* @returns sessionId for the outbound session.
*/
public async createOutboundSession(theirIdentityKey: string, theirOneTimeKey: string): Promise<string> {
let newSessionId: string;
@ -612,15 +578,14 @@ export class OlmDevice {
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} messageType messageType field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
* @param theirDeviceIdentityKey - remote user's Curve25519 identity key
* @param messageType - messageType field from the received message (must be 0)
* @param ciphertext - base64-encoded body from the received message
*
* @return {{payload: string, session_id: string}} decrypted payload, and
* @returns decrypted payload, and
* session id of new session
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
* @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key).
*/
public async createInboundSession(
theirDeviceIdentityKey: string,
@ -673,9 +638,9 @@ export class OlmDevice {
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* @param theirDeviceIdentityKey - Curve25519 identity key for the
* remote device
* @return {Promise<string[]>} a list of known session ids for the device
* @returns a list of known session ids for the device
*/
public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
const log = logger.withPrefix("[getSessionIdsForDevice]");
@ -708,13 +673,13 @@ export class OlmDevice {
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* @param theirDeviceIdentityKey - Curve25519 identity key for the
* remote device
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* @param 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 {PrefixedLogger} [log] A possibly customised log
* @return {Promise<?string>} session id, or null if no established session
* @param log - A possibly customised log
* @returns session id, or null if no established session
*/
public async getSessionIdForDevice(
theirDeviceIdentityKey: string,
@ -756,12 +721,11 @@ export class OlmDevice {
* the keys 'hasReceivedMessage' (true if the session has received an incoming
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* @param deviceIdentityKey - Curve25519 identity key for the device
* @param 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
* @return {Array.<{sessionId: string, hasReceivedMessage: boolean}>}
* @param log - A possibly customised log
*/
public async getSessionInfoForDevice(
deviceIdentityKey: string,
@ -810,21 +774,21 @@ export class OlmDevice {
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* @param theirDeviceIdentityKey - Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
* @param sessionId - the id of the active session
* @param payloadString - payload to be encrypted and sent
*
* @return {Promise<string>} ciphertext
* @returns ciphertext
*/
public async encryptMessage(
theirDeviceIdentityKey: string,
sessionId: string,
payloadString: string,
): Promise<string> {
): Promise<IMessage> {
checkPayloadLength(payloadString);
let res;
let res: IMessage;
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
@ -840,19 +804,19 @@ export class OlmDevice {
},
logger.withPrefix("[encryptMessage]"),
);
return res;
return res!;
}
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* @param theirDeviceIdentityKey - Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} messageType messageType field from the received message
* @param {string} ciphertext base64-encoded body from the received message
* @param sessionId - the id of the active session
* @param messageType - messageType field from the received message
* @param ciphertext - base64-encoded body from the received message
*
* @return {Promise<string>} decrypted payload.
* @returns decrypted payload.
*/
public async decryptMessage(
theirDeviceIdentityKey: string,
@ -860,7 +824,7 @@ export class OlmDevice {
messageType: number,
ciphertext: string,
): Promise<string> {
let payloadString;
let payloadString: string;
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
@ -877,19 +841,19 @@ export class OlmDevice {
},
logger.withPrefix("[decryptMessage]"),
);
return payloadString;
return payloadString!;
}
/**
* Determine if an incoming messages is a prekey message matching an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* @param theirDeviceIdentityKey - Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} messageType messageType field from the received message
* @param {string} ciphertext base64-encoded body from the received message
* @param sessionId - the id of the active session
* @param messageType - messageType field from the received message
* @param ciphertext - base64-encoded body from the received message
*
* @return {Promise<boolean>} true if the received message is a prekey message which matches
* @returns true if the received message is a prekey message which matches
* the given session.
*/
public async matchesSession(
@ -902,7 +866,7 @@ export class OlmDevice {
return false;
}
let matches;
let matches: boolean;
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
@ -912,7 +876,7 @@ export class OlmDevice {
},
logger.withPrefix("[matchesSession]"),
);
return matches;
return matches!;
}
public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
@ -934,8 +898,7 @@ export class OlmDevice {
/**
* store an OutboundGroupSession in outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
* @internal
*/
private saveOutboundGroupSession(session: OutboundGroupSession): void {
this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey);
@ -945,10 +908,8 @@ export class OlmDevice {
* extract an OutboundGroupSession from outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
* @returns result of func
* @internal
*/
private getOutboundGroupSession<T>(sessionId: string, func: (session: OutboundGroupSession) => T): T {
const pickled = this.outboundGroupSessionStore[sessionId];
@ -968,7 +929,7 @@ export class OlmDevice {
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
* @returns sessionId for the outbound session.
*/
public createOutboundGroupSession(): string {
const session = new global.Olm.OutboundGroupSession();
@ -984,10 +945,10 @@ export class OlmDevice {
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
* @param sessionId - the id of the outboundgroupsession
* @param payloadString - payload to be encrypted and sent
*
* @return {string} ciphertext
* @returns ciphertext
*/
public encryptGroupMessage(sessionId: string, payloadString: string): string {
logger.log(`encrypting msg with megolm session ${sessionId}`);
@ -1004,9 +965,9 @@ export class OlmDevice {
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
* @param sessionId - the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* @returns current chain index, and
* base64-encoded secret key.
*/
public getOutboundGroupSessionKey(sessionId: string): IOutboundGroupSessionKey {
@ -1025,9 +986,9 @@ export class OlmDevice {
* Unpickle a session from a sessionData object and invoke the given function.
* The session is valid only until func returns.
*
* @param {Object} sessionData Object describing the session.
* @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session
* @return {*} result of func
* @param sessionData - Object describing the session.
* @param func - Invoked with the unpickled session
* @returns result of func
*/
private unpickleInboundGroupSession<T>(
sessionData: InboundGroupSessionData,
@ -1045,15 +1006,12 @@ export class OlmDevice {
/**
* extract an InboundGroupSession from the crypto store and call the given function
*
* @param {string} roomId The room ID to extract the session for, or null to fetch
* @param roomId - The room ID to extract the session for, or null to fetch
* sessions for any room.
* @param {string} senderKey
* @param {string} sessionId
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func
* function to call.
* @param txn - Opaque transaction object from cryptoStore.doTxn()
* @param func - function to call.
*
* @private
* @internal
*/
private getInboundGroupSession(
roomId: string,
@ -1092,16 +1050,16 @@ export class OlmDevice {
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding
* @param roomId - room in which this session will be used
* @param senderKey - base64-encoded curve25519 key of the sender
* @param forwardingCurve25519KeyChain - Devices involved in forwarding
* this session to us.
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
* @param {boolean} exportFormat true if the megolm keys are in export format
* @param sessionId - session identifier
* @param sessionKey - base64-encoded secret key
* @param keysClaimed - Other keys the sender claims.
* @param exportFormat - true if the megolm keys are in export format
* (ie, they lack an ed25519 signature)
* @param {Object} [extraSessionData={}] any other data to be include with the session
* @param extraSessionData - any other data to be include with the session
*/
public async addInboundGroupSession(
roomId: string,
@ -1213,11 +1171,11 @@ export class OlmDevice {
/**
* Record in the data store why an inbound group session was withheld.
*
* @param {string} roomId room that the session belongs to
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} code reason code
* @param {string} reason human-readable version of `code`
* @param roomId - room that the session belongs to
* @param senderKey - base64-encoded curve25519 key of the sender
* @param sessionId - session identifier
* @param code - reason code
* @param reason - human-readable version of `code`
*/
public async addInboundGroupSessionWithheld(
roomId: string,
@ -1245,18 +1203,14 @@ export class OlmDevice {
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
* @param {string} eventId ID of the event being decrypted
* @param {Number} timestamp timestamp of the event being decrypted
* @param roomId - room in which the message was received
* @param senderKey - base64-encoded curve25519 key of the sender
* @param sessionId - session identifier
* @param body - base64-encoded body of the encrypted message
* @param eventId - ID of the event being decrypted
* @param timestamp - timestamp of the event being decrypted
*
* @return {null} the sessionId is unknown
*
* @return {Promise<{result: string, senderKey: string,
* forwardingCurve25519KeyChain: Array<string>,
* keysClaimed: Object<string, string>}>}
* @returns null if the sessionId is unknown
*/
public async decryptGroupMessage(
roomId: string,
@ -1293,7 +1247,7 @@ export class OlmDevice {
result = null;
return;
}
let res;
let res: ReturnType<InboundGroupSession["decrypt"]>;
try {
res = session.decrypt(body);
} catch (e) {
@ -1313,8 +1267,8 @@ export class OlmDevice {
let plaintext: string = res.plaintext;
if (plaintext === undefined) {
// Compatibility for older olm versions.
plaintext = res;
// @ts-ignore - Compatibility for older olm versions.
plaintext = res as string;
} else {
// Check if we have seen this message index before to detect replay attacks.
// If the event ID and timestamp are specified, and the match the event ID
@ -1372,11 +1326,11 @@ export class OlmDevice {
/**
* Determine if we have the keys for a given megolm session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param roomId - room in which the message was received
* @param senderKey - base64-encoded curve25519 key of the sender
* @param sessionId - session identifier
*
* @returns {Promise<boolean>} true if we have the keys to this session
* @returns true if we have the keys to this session
*/
public async hasInboundSessionKeys(roomId: string, senderKey: string, sessionId: string): Promise<boolean> {
let result: boolean;
@ -1415,16 +1369,13 @@ export class OlmDevice {
/**
* Extract the keys to a given megolm session, for sharing
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {number} chainIndex The chain index at which to export the session.
* @param roomId - room in which the message was received
* @param senderKey - base64-encoded curve25519 key of the sender
* @param sessionId - session identifier
* @param chainIndex - The chain index at which to export the session.
* If omitted, export at the first index we know about.
*
* @returns {Promise<{chain_index: number, key: string,
* forwarding_curve25519_key_chain: Array<string>,
* sender_claimed_ed25519_key: string
* }>}
* @returns
* details of the session key. The key is a base64-encoded megolm key in
* export format.
*
@ -1489,10 +1440,10 @@ export class OlmDevice {
/**
* Export an inbound group session
*
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {ISessionInfo} sessionData The session object from the store
* @return {module:crypto/OlmDevice.MegolmSessionData} exported session data
* @param senderKey - base64-encoded curve25519 key of the sender
* @param sessionId - session identifier
* @param sessionData - The session object from the store
* @returns exported session data
*/
public exportInboundGroupSession(
senderKey: string,
@ -1536,11 +1487,11 @@ export class OlmDevice {
/**
* Verify an ed25519 signature.
*
* @param {string} key ed25519 key
* @param {string} message message which was signed
* @param {string} signature base64-encoded signature to be checked
* @param key - ed25519 key
* @param message - message which was signed
* @param signature - base64-encoded signature to be checked
*
* @raises {Error} if there is a problem with the verification. If the key was
* @throws Error if there is a problem with the verification. If the key was
* too small then the message will be "OLM.INVALID_BASE64". If the signature
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
@ -1555,7 +1506,7 @@ export class OlmDevice {
}
}
export const WITHHELD_MESSAGES = {
export const WITHHELD_MESSAGES: Record<string, string> = {
"m.unverified": "The sender has disabled encrypting to unverified devices.",
"m.blacklisted": "The sender has blocked you.",
"m.unauthorised": "You are not authorised to read the message.",
@ -1565,11 +1516,11 @@ export const WITHHELD_MESSAGES = {
/**
* Calculate the message to use for the exception when a session key is withheld.
*
* @param {object} withheld An object that describes why the key was withheld.
* @param withheld - An object that describes why the key was withheld.
*
* @return {string} the message
* @returns the message
*
* @private
* @internal
*/
function calculateWithheldMessage(withheld: IWithheld): string {
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {

View File

@ -14,28 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { v4 as uuidv4 } from "uuid";
import { logger } from '../logger';
import { MatrixClient } from "../client";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
import { CryptoStore, OutgoingRoomKeyRequest } from './store/base';
import { EventType } from "../@types/event";
import { EventType, ToDeviceMessageId } from "../@types/event";
/**
* Internal module. Management of outgoing room key requests.
*
* See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
* for draft documentation on what we're supposed to be implementing here.
*
* @module
*/
// delay between deciding we want some keys, and sending out the request, to
// allow for (a) it turning up anyway, (b) grouping requests together
const SEND_KEY_REQUESTS_DELAY_MS = 500;
/** possible states for a room key request
/**
* possible states for a room key request
*
* The state machine looks like:
* ```
*
* | (cancellation sent)
* | .-------------------------------------------------.
@ -58,8 +60,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
* | (cancellation sent) |
* V |
* (deleted) <---------------------------+
*
* @enum {number}
* ```
*/
export enum RoomKeyRequestState {
/** request not yet sent */
@ -132,12 +133,10 @@ export class OutgoingRoomKeyRequestManager {
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
* @param {boolean} resend whether to resend the key request if there is
* @param resend - whether to resend the key request if there is
* already one
*
* @returns {Promise} resolves when the request has been added to the
* @returns resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
@ -237,9 +236,8 @@ export class OutgoingRoomKeyRequestManager {
/**
* Cancel room key requests, if any match the given requestBody
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
*
* @returns {Promise} resolves when the request has been updated in our
* @returns resolves when the request has been updated in our
* pending list.
*/
public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<unknown> {
@ -322,11 +320,10 @@ export class OutgoingRoomKeyRequestManager {
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
* @param userId - Target user ID
* @param deviceId - Target device ID
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* @returns resolves to a list of all the {@link OutgoingRoomKeyRequest}
*/
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
@ -337,7 +334,7 @@ export class OutgoingRoomKeyRequestManager {
* This is intended for situations where something substantial has changed, and we
* don't really expect the other end to even care about the cancellation.
* For example, after initialization or self-verification.
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
* @returns An array of `queueRoomKeyRequest` outputs.
*/
public async cancelAndResendAllOutgoingRequests(): Promise<void[]> {
const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
@ -483,7 +480,10 @@ export class OutgoingRoomKeyRequestManager {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
}
contentMap[recip.userId][recip.deviceId] = message;
contentMap[recip.userId][recip.deviceId] = {
...message,
[ToDeviceMessageId]: uuidv4(),
};
}
return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);

View File

@ -15,8 +15,6 @@ limitations under the License.
*/
/**
* @module crypto/RoomList
*
* Manages the list of encrypted rooms
*/
@ -31,9 +29,6 @@ export interface IRoomEncryption {
}
/* eslint-enable camelcase */
/**
* @alias module:crypto/RoomList
*/
export class RoomList {
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
private roomEncryption: Record<string, IRoomEncryption> = {};

View File

@ -14,17 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring';
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { ICryptoCallbacks } from ".";
import { ICryptoCallbacks, IEncryptedContent } from ".";
import { IContent, MatrixEvent } from "../models/event";
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { TypedEventEmitter } from '../models/typed-event-emitter';
import { defer, IDeferred } from "../utils";
import { ToDeviceMessageId } from "../@types/event";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@ -58,14 +60,12 @@ interface IDecryptors {
interface ISecretInfo {
encrypted: {
// eslint-disable-next-line camelcase
key_id: IEncryptedPayload;
[keyId: string]: IEncryptedPayload;
};
}
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
private requests = new Map<string, ISecretRequestInternal>();
@ -118,15 +118,15 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Add a key for encrypting secrets.
*
* @param {string} algorithm the algorithm used by the key.
* @param {object} opts the options for the algorithm. The properties used
* @param algorithm - the algorithm used by the key.
* @param opts - the options for the algorithm. The properties used
* depend on the algorithm given.
* @param {string} [keyId] the ID of the key. If not given, a random
* @param keyId - the ID of the key. If not given, a random
* ID will be generated.
*
* @return {object} An object with:
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
* @returns An object with:
* keyId: the ID of the key
* keyInfo: details about the key (iv, mac, passphrase)
*/
public async addKey(
algorithm: string,
@ -175,9 +175,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Get the key information for a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to check
* @param keyId - The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* @returns If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/
@ -198,9 +198,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Check whether we have a key with a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to check
* @param keyId - The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
* @returns Whether we have the key.
*/
public async hasKey(keyId?: string): Promise<boolean> {
return Boolean(await this.getKey(keyId));
@ -209,10 +209,10 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Check whether a key matches what we expect based on the key info
*
* @param {Uint8Array} key the key to check
* @param {object} info the key info
* @param key - the key to check
* @param info - the key info
*
* @return {boolean} whether or not the key matches
* @returns whether or not the key matches
*/
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
@ -231,9 +231,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Store an encrypted secret on the server
*
* @param {string} name The name of the secret
* @param {string} secret The secret contents.
* @param {Array} keys The IDs of the keys to use to encrypt the secret
* @param name - The name of the secret
* @param secret - The secret contents.
* @param 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[] | null): Promise<void> {
@ -279,9 +279,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Get a secret from storage.
*
* @param {string} name the name of the secret
* @param name - the name of the secret
*
* @return {string} the contents of the secret
* @returns the contents of the secret
*/
public async get(name: string): Promise<string | undefined> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
@ -315,31 +315,19 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
`the keys it is encrypted with are for a supported algorithm`);
}
let keyId: string;
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
// fetch private key from app
const [keyId, decryption] = await this.getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId];
const encInfo = secretInfo.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself. It must be base64
// encoded, since this is how a key would normally be stored.
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
return decryption.decrypt(encInfo);
} finally {
if (decryption && decryption.free) decryption.free();
}
return decryption.decrypt(encInfo);
}
/**
* Check if a secret is stored on the server.
*
* @param {string} name the name of the secret
* @param name - the name of the secret
*
* @return {object?} map of key name to key info the secret is encrypted
* @returns map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
@ -348,7 +336,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo?.encrypted) return null;
const ret = {};
const ret: Record<string, ISecretStorageKeyInfo> = {};
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
@ -372,8 +360,8 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
/**
* Request a secret from another device
*
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
* @param name - the name of the secret to request
* @param devices - the devices to request the secret from
*/
public request(this: SecretStorage<MatrixClient>, name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
@ -388,7 +376,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
const toDevice: Record<string, typeof cancelData> = {};
for (const device of devices) {
toDevice[device] = cancelData;
}
@ -407,8 +395,9 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
action: "request",
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
[ToDeviceMessageId]: uuidv4(),
};
const toDevice = {};
const toDevice: Record<string, typeof requestData> = {};
for (const device of devices) {
toDevice[device] = requestData;
}
@ -486,10 +475,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
secret: secret,
},
};
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key,
sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
await olmlib.ensureOlmSessionsForDevices(
this.baseApis.crypto!.olmDevice,

View File

@ -22,18 +22,21 @@ const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
/**
* encrypt a string
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
* @param data - the plaintext to encrypt
* @param key - the encryption key to use
* @param name - the name of the secret
* @param ivStr - the initialization vector to use
*/
export async function encryptAES(
data: string,
@ -83,12 +86,9 @@ export async function encryptAES(
/**
* decrypt a string
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param data - the encrypted data
* @param key - the encryption key to use
* @param name - the name of the secret
*/
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
@ -168,10 +168,10 @@ const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
/** Calculate the MAC for checking the key.
*
* @param {Uint8Array} key the key to use
* @param {string} [iv] The initialization vector as a base64-encoded string.
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @return {Promise<object>} An object that contains, `mac` and `iv` properties.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
return encryptAES(ZERO_STR, key, "", iv);

View File

@ -16,59 +16,53 @@ limitations under the License.
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module
*/
import { MatrixClient } from "../../client";
import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { IContent, MatrixEvent, RoomMember } from "../../matrix";
import {
Crypto,
IEncryptedContent,
IEventDecryptionResult,
IMegolmSessionData,
IncomingRoomKeyRequest,
} from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
* Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
*/
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
export type DecryptionClassParams<P extends IParams = IParams> = Omit<P, "deviceId" | "config">;
/**
* map of registered encryption algorithm classes. Map from string to {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
* map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class
*/
export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
export interface IParams {
/** The UserID for the local user */
userId: string;
/** The identifier for this device. */
deviceId: string;
/** crypto core */
crypto: Crypto;
/** olm.js wrapper */
olmDevice: OlmDevice;
/** base matrix api interface */
baseApis: MatrixClient;
/** The ID of the room we will be sending to */
roomId?: string;
/** The body of the m.room.encryption event */
config: IRoomEncryption & object;
}
/**
* base type for encryption implementations
*
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {MatrixClient} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
export abstract class EncryptionAlgorithm {
protected readonly userId: string;
@ -78,6 +72,9 @@ export abstract class EncryptionAlgorithm {
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
/**
* @param params - parameters
*/
public constructor(params: IParams) {
this.userId = params.userId;
this.deviceId = params.deviceId;
@ -91,33 +88,28 @@ export abstract class EncryptionAlgorithm {
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
* @param room - the room the event is in
*/
public prepareToEncrypt(room: Room): void {}
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
* @public
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content event content
* @param content - event content
*
* @return {Promise} Promise which resolves to the new event body
* @returns Promise which resolves to the new event body
*/
public abstract encryptMessage(room: Room, eventType: string, content: object): Promise<object>;
public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise<IEncryptedContent>;
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
* @param event - event causing the change
* @param member - user whose membership changed
* @param oldMembership - previous membership
* @public
* @abstract
*/
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
@ -133,15 +125,6 @@ export abstract class EncryptionAlgorithm {
/**
* base type for decryption implementations
*
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {MatrixClient} baseApis base matrix api interface
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
export abstract class DecryptionAlgorithm {
protected readonly userId: string;
@ -161,12 +144,9 @@ export abstract class DecryptionAlgorithm {
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
* @param event - undecrypted event
*
* @param {MatrixEvent} event undecrypted event
*
* @return {Promise<module:crypto~EventDecryptionResult>} promise which
* @returns promise which
* resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
*/
@ -175,9 +155,7 @@ export abstract class DecryptionAlgorithm {
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
* @param params - event key event
*/
public async onRoomKeyEvent(params: MatrixEvent): Promise<void> {
// ignore by default
@ -186,8 +164,7 @@ export abstract class DecryptionAlgorithm {
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
* @param {object} opts object
* @param opts - object
*/
public async importRoomKey(session: IMegolmSessionData, opts: object): Promise<void> {
// ignore by default
@ -196,8 +173,7 @@ export abstract class DecryptionAlgorithm {
/**
* Determine if we have the keys necessary to respond to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
* @return {Promise<boolean>} true if we have the keys and could (theoretically) share
* @returns true if we have the keys and could (theoretically) share
* them; else false.
*/
public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
@ -207,7 +183,6 @@ export abstract class DecryptionAlgorithm {
/**
* Send the response to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
*/
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
@ -217,7 +192,7 @@ export abstract class DecryptionAlgorithm {
* Retry decrypting all the events from a sender that haven't been
* decrypted yet.
*
* @param {string} senderKey the sender's key
* @param senderKey - the sender's key
*/
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
// ignore by default
@ -231,13 +206,10 @@ export abstract class DecryptionAlgorithm {
/**
* Exception thrown when decryption fails
*
* @alias module:crypto/algorithms/base.DecryptionError
* @param {string} msg user-visible message describing the problem
* @param msg - user-visible message describing the problem
*
* @param {Object=} details key/value pairs reported in the logs but not shown
* @param details - key/value pairs reported in the logs but not shown
* to the user.
*
* @extends Error
*/
export class DecryptionError extends Error {
public readonly detailedString: string;
@ -262,16 +234,14 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
return result;
}
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
export class UnknownDeviceError extends Error {
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @param msg - message describing the problem
* @param devices - set of unknown devices per user we're warning about
*/
public constructor(
msg: string,
public readonly devices: Record<string, Record<string, object>>,
@ -286,15 +256,11 @@ export class UnknownDeviceError extends Error {
/**
* Registers an encryption/decryption class for a particular algorithm
*
* @param {string} algorithm algorithm tag to register for
* @param algorithm - algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
* @param encryptor - {@link EncryptionAlgorithm} implementation
*
* @param {class} decryptor {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
* @param decryptor - {@link DecryptionAlgorithm} implementation
*/
export function registerAlgorithm<P extends IParams = IParams>(
algorithm: string,

View File

@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module crypto/algorithms
*/
import "./olm";
import "./megolm";

View File

@ -16,10 +16,10 @@ limitations under the License.
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
import { v4 as uuidv4 } from "uuid";
import { logger } from '../../logger';
import * as olmlib from "../olmlib";
import {
@ -36,9 +36,15 @@ import { Room } from '../../models/room';
import { DeviceInfo } from "../deviceinfo";
import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../../models/event";
import { EventType, MsgType } from '../../@types/event';
import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { IContent, MatrixEvent } from "../../models/event";
import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event';
import {
IMegolmEncryptedContent,
IEventDecryptionResult,
IMegolmSessionData,
IncomingRoomKeyRequest,
IEncryptedContent,
} from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api";
@ -115,37 +121,27 @@ interface SharedWithData {
}
/**
* @private
* @constructor
*
* @param {string} sessionId
* @param {boolean} sharedHistory whether the session can be freely shared with
* other group members, according to the room history visibility settings
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
* @property {Number} creationTime when the session was created (ms since the epoch)
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> SharedWithData}
* @internal
*/
class OutboundSessionInfo {
/** number of times this session has been used */
public useCount = 0;
/** when the session was created (ms since the epoch) */
public creationTime: number;
/** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
/**
* @param sharedHistory - whether the session can be freely shared with
* other group members, according to the room history visibility settings
*/
public constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
this.creationTime = new Date().getTime();
}
/**
* Check if it's time to rotate the session
*
* @param {Number} rotationPeriodMsgs
* @param {Number} rotationPeriodMs
* @return {Boolean}
*/
public needsRotation(rotationPeriodMsgs: number, rotationPeriodMs: number): boolean {
const sessionLifetime = new Date().getTime() - this.creationTime;
@ -181,10 +177,10 @@ class OutboundSessionInfo {
* Determine if this session has been shared with devices which it shouldn't
* have been.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* @param devicesInRoom - `userId -> {deviceId -> object}`
* devices we should shared the session with.
*
* @return {Boolean} true if we have shared the session with devices which aren't
* @returns true if we have shared the session with devices which aren't
* in devicesInRoom.
*/
public sharedWithTooManyDevices(devicesInRoom: Record<string, Record<string, object>>): boolean {
@ -220,13 +216,9 @@ class OutboundSessionInfo {
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/EncryptionAlgorithm}
* @param params - parameters, as per {@link EncryptionAlgorithm}
*/
class MegolmEncryption extends EncryptionAlgorithm {
export class MegolmEncryption extends EncryptionAlgorithm {
// the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we
// are using, and which devices we have shared the keys with. It resolves
@ -257,15 +249,14 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @private
* @internal
*
* @param {module:models/room} room
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
* @param devicesInRoom - The devices in this room, indexed by user ID
* @param blocked - The devices that are blocked, indexed by user ID
* @param singleOlmCreationPhase - Only perform one round of olm
* session creation
*
* @return {Promise} Promise which resolves to the
* @returns Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
private async ensureOutboundSession(
@ -480,11 +471,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @private
* @internal
*
* @param {boolean} sharedHistory
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
* @returns session
*/
private async prepareNewSession(sharedHistory: boolean): Promise<OutboundSessionInfo> {
const sessionId = this.olmDevice.createOutboundGroupSession();
@ -506,15 +496,15 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Determines what devices in devicesByUser don't have an olm session as given
* in devicemap.
*
* @private
* @internal
*
* @param {object} devicemap the devices that have olm sessions, as returned by
* @param devicemap - the devices that have olm sessions, as returned by
* olmlib.ensureOlmSessionsForDevices.
* @param {object} devicesByUser a map of user IDs to array of deviceInfo
* @param {array} [noOlmDevices] an array to fill with devices that don't have
* @param devicesByUser - a map of user IDs to array of deviceInfo
* @param noOlmDevices - an array to fill with devices that don't have
* olm sessions
*
* @return {array} an array of devices that don't have olm sessions. If
* @returns an array of devices that don't have olm sessions. If
* noOlmDevices is specified, then noOlmDevices will be returned.
*/
private getDevicesWithoutSessions(
@ -550,11 +540,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call.
*
* @private
* @internal
*
* @param {object} devicesByUser map from userid to list of devices
* @param devicesByUser - map from userid to list of devices
*
* @return {array<array<object>>} the blocked devices, split into chunks
* @returns the blocked devices, split into chunks
*/
private splitDevices<T extends DeviceInfo | IBlockedDevice>(
devicesByUser: Record<string, Record<string, { device: T }>>,
@ -591,18 +581,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @private
* @internal
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {number} chainIndex current chain index
* @param chainIndex - current chain index
*
* @param {object<userId, deviceInfo>} userDeviceMap
* mapping from userId to deviceInfo
* @param userDeviceMap - mapping from userId to deviceInfo
*
* @param {object} payload fields to include in the encrypted payload
* @param payload - fields to include in the encrypted payload
*
* @return {Promise} Promise which resolves once the key sharing
* @returns Promise which resolves once the key sharing
* for the given userDeviceMap is generated and has been sent.
*/
private encryptAndSendKeysToDevices(
@ -631,15 +619,14 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @private
* @internal
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {array<object>} userDeviceMap list of blocked devices to notify
* @param userDeviceMap - list of blocked devices to notify
*
* @param {object} payload fields to include in the notification payload
* @param payload - fields to include in the notification payload
*
* @return {Promise} Promise which resolves once the notifications
* @returns Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
private async sendBlockedNotificationsToDevices(
@ -655,9 +642,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
const message = Object.assign({}, payload);
message.code = blockedInfo.code;
message.reason = blockedInfo.reason;
const message = {
...payload,
code: blockedInfo.code,
reason: blockedInfo.reason,
[ToDeviceMessageId]: uuidv4(),
};
if (message.code === "m.no_olm") {
delete message.room_id;
delete message.session_id;
@ -683,10 +674,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
*
* @param {string} senderKey The key of the originating device for the session
* @param {string} sessionId ID of the outbound session to share
* @param {string} userId ID of the user who owns the target device
* @param {module:crypto/deviceinfo} device The target device
* @param senderKey - The key of the originating device for the session
* @param sessionId - ID of the outbound session to share
* @param userId - ID of the user who owns the target device
* @param device - The target device
*/
public async reshareKeyWithDevice(
senderKey: string,
@ -755,10 +746,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
},
};
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
@ -779,26 +771,23 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @private
* @internal
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key the session key as returned by
* @param key - the session key as returned by
* OlmDevice.getOutboundGroupSessionKey
*
* @param {object} payload the base to-device message payload for sharing keys
* @param payload - the base to-device message payload for sharing keys
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
* @param devicesByUser - map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* @param errorDevices - array that will be populated with the devices that we can't get an
* olm session for
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* @param otkTimeout - The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* @param failedServers - An array to fill with remote servers that
* failed to respond to one-time-key requests.
*/
private async shareKeyWithDevices(
@ -853,11 +842,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
/**
* Notify devices that we weren't able to create olm sessions.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key
*
* @param {Array<object>} failedDevices the devices that we were unable to
* @param failedDevices - the devices that we were unable to
* create olm sessions for, as returned by shareKeyWithDevices
*/
private async notifyFailedOlmDevices(
@ -914,10 +901,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
/**
* Notify blocked devices that they have been blocked.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, object>} devicesByUser
* map from userid to device ID to blocked data
* @param devicesByUser - map from userid to device ID to blocked data
*/
private async notifyBlockedDevices(
session: OutboundSessionInfo,
@ -950,7 +935,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
* @param room - the room the event is in
*/
public prepareToEncrypt(room: Room): void {
if (this.encryptionPreparation != null) {
@ -995,15 +980,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
/**
* @inheritdoc
* @param content - plaintext event content
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {Promise} Promise which resolves to the new event body
* @returns Promise which resolves to the new event body
*/
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IMegolmEncryptedContent> {
logger.log(`Starting to encrypt event for ${this.roomId}`);
if (this.encryptionPreparation != null) {
@ -1038,12 +1019,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
content: content,
};
const ciphertext = this.olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson));
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
@ -1057,7 +1036,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
return encryptedContent;
}
private isVerificationEvent(eventType: string, content: object): boolean {
private isVerificationEvent(eventType: string, content: IContent): boolean {
switch (eventType) {
case EventType.KeyVerificationCancel:
case EventType.KeyVerificationDone:
@ -1092,7 +1071,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
* unknown to the user. If so, warn the user, and mark them as known to
* give the user a chance to go verify them before re-sending this message.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* @param devicesInRoom - `userId -> {deviceId -> object}`
* devices we should shared the session with.
*/
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
@ -1122,7 +1101,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
* Remove unknown devices from a set of devices. The devicesInRoom parameter
* will be modified.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* @param devicesInRoom - `userId -> {deviceId -> object}`
* devices we should shared the session with.
*/
private removeUnknownDevices(devicesInRoom: DeviceInfoMap): void {
@ -1142,11 +1121,10 @@ 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
* @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
* @returns Promise which resolves to an array whose
* first element is a map from userId to deviceId to deviceInfo indicating
* the devices that messages should be encrypted to, and whose second
* element is a map from userId to deviceId to data indicating the devices
@ -1214,13 +1192,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/DecryptionAlgorithm}
* @param params - parameters, as per {@link DecryptionAlgorithm}
*/
class MegolmDecryption extends DecryptionAlgorithm {
export class MegolmDecryption extends DecryptionAlgorithm {
// events which we couldn't decrypt due to unknown sessions /
// indexes, or which we could only decrypt with untrusted keys:
// map from senderKey|sessionId to Set of MatrixEvents
@ -1237,12 +1211,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
}
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* {@link EventDecryptionResult} once we have finished
* decrypting, or rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
@ -1383,9 +1353,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
/**
* Add an event to the list of those awaiting their session keys.
*
* @private
* @internal
*
* @param {module:models/event.MatrixEvent} event
*/
private addEventToPendingList(event: MatrixEvent): void {
const content = event.getWireContent();
@ -1404,9 +1373,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
/**
* Remove an event from the list of those awaiting their session keys.
*
* @private
* @internal
*
* @param {module:models/event.MatrixEvent} event
*/
private removeEventFromPendingList(event: MatrixEvent): void {
const content = event.getWireContent();
@ -1427,11 +1395,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
}
}
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent<Partial<IMessage["content"]>>();
let senderKey = event.getSenderKey()!;
@ -1608,9 +1571,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
* @param event - key event
*/
public async onRoomKeyWithheldEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent();
@ -1663,10 +1624,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
await olmlib.ensureOlmSessionsForDevices(
this.olmDevice, this.baseApis, { [sender]: [device] }, false,
);
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
@ -1694,9 +1656,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
}
}
/**
* @inheritdoc
*/
public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
const body = keyRequest.requestBody;
@ -1708,9 +1667,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
);
}
/**
* @inheritdoc
*/
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
@ -1744,10 +1700,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
body.room_id, body.sender_key, body.session_id,
);
}).then((payload) => {
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
return this.olmlib.encryptMessageForDevice(
@ -1795,19 +1752,15 @@ class MegolmDecryption extends DecryptionAlgorithm {
}
/**
* @inheritdoc
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
* @param {object} [opts={}] options for the import
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
* @param {string} [opts.source] where the key came from
* @param untrusted - whether the key should be considered as untrusted
* @param source - where the key came from
*/
public importRoomKey(
session: IMegolmSessionData,
opts: { untrusted?: boolean, source?: string } = {},
{ untrusted, source }: { untrusted?: boolean, source?: string } = {},
): Promise<void> {
const extraSessionData: OlmGroupSessionExtraData = {};
if (opts.untrusted || session.untrusted) {
if (untrusted || session.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {
@ -1823,7 +1776,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
true,
extraSessionData,
).then(() => {
if (opts.source !== "backup") {
if (source !== "backup") {
// don't wait for it to complete
this.crypto.backupManager.backupGroupSession(
session.sender_key, session.session_id,
@ -1842,13 +1795,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
* Have another go at decrypting events after we receive a key. Resolves once
* decryption has been re-attempted on all events.
*
* @private
* @param {String} senderKey
* @param {String} sessionId
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
* @internal
* @param forceRedecryptIfUntrusted - whether messages that were already
* successfully decrypted using untrusted keys should be re-decrypted
*
* @return {Boolean} whether all messages were successfully
* @returns whether all messages were successfully
* decrypted with trusted keys
*/
private async retryDecryption(
@ -1923,6 +1874,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
promises.push(

View File

@ -16,8 +16,6 @@ limitations under the License.
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
import { logger } from '../../logger';
@ -30,13 +28,13 @@ import {
registerAlgorithm,
} from "./base";
import { Room } from '../../models/room';
import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult } from "../index";
import { IContent, MatrixEvent } from "../../models/event";
import { IEncryptedContent, IEventDecryptionResult, IOlmEncryptedContent } from "../index";
import { IInboundSession } from "../OlmDevice";
const DeviceVerification = DeviceInfo.DeviceVerification;
interface IMessage {
export interface IMessage {
type: number;
body: string;
}
@ -44,21 +42,17 @@ interface IMessage {
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/EncryptionAlgorithm}
* @param params - parameters, as per {@link EncryptionAlgorithm}
*/
class OlmEncryption extends EncryptionAlgorithm {
private sessionPrepared = false;
private prepPromise: Promise<void> | null = null;
/**
* @private
* @internal
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {Promise} Promise which resolves when setup is complete
* @param roomMembers - list of currently-joined users in the room
* @returns Promise which resolves when setup is complete
*/
private ensureSession(roomMembers: string[]): Promise<void> {
if (this.prepPromise) {
@ -83,15 +77,11 @@ class OlmEncryption extends EncryptionAlgorithm {
}
/**
* @inheritdoc
* @param content - plaintext event content
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {Promise} Promise which resolves to the new event body
* @returns Promise which resolves to the new event body
*/
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IOlmEncryptedContent> {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
@ -111,9 +101,9 @@ class OlmEncryption extends EncryptionAlgorithm {
content: content,
};
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
sender_key: this.olmDevice.deviceCurve25519Key!,
ciphertext: {},
};
@ -150,19 +140,12 @@ class OlmEncryption extends EncryptionAlgorithm {
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/DecryptionAlgorithm}
* @param params - parameters, as per {@link DecryptionAlgorithm}
*/
class OlmDecryption extends DecryptionAlgorithm {
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* {@link EventDecryptionResult} once we have finished
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
@ -275,10 +258,10 @@ class OlmDecryption extends DecryptionAlgorithm {
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
* @param theirDeviceIdentityKey - Curve25519 identity key of the sender
* @param message - message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
* @returns payload, if decrypted successfully.
*/
private decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
// This is a wrapper that serialises decryptions of prekey messages, because

View File

@ -66,8 +66,7 @@ export interface IRecoveryKey {
export interface ICreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* Returns:
* {Promise<Object>} Object with public key metadata, encoded private
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
@ -131,6 +130,7 @@ export interface IImportOpts {
}
export interface IImportRoomKeysOpts {
/** called with an object that has a "stage" param */
progressCallback?: (stage: IImportOpts) => void;
untrusted?: boolean;
source?: string; // TODO: Enum

View File

@ -15,8 +15,6 @@ limitations under the License.
*/
/**
* @module crypto/backup
*
* Classes for dealing with key backup.
*/
@ -93,7 +91,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
@ -134,7 +132,7 @@ export class BackupManager {
*
* Throws an error if a problem is detected.
*
* @param {IKeyBackupInfo} info the key backup info
* @param info - the key backup info
*/
public static checkBackupVersion(info: IKeyBackupInfo): void {
const Algorithm = algorithmsByName[info.algorithm];
@ -276,7 +274,7 @@ export class BackupManager {
* Forces a re-check of the key backup and enables/disables it
* as appropriate.
*
* @return {Object} Object with backup info (as returned by
* @returns Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
@ -309,15 +307,7 @@ export class BackupManager {
/**
* Check if the given backup info is trusted.
*
* @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
* deviceId: [string],
* device: [DeviceInfo || null],
* ]
* }
* @param backupInfo - key backup info dict from /room_keys/version
*/
public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise<TrustInfo> {
const ret = {
@ -432,7 +422,7 @@ export class BackupManager {
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
* @param maxDelay - Maximum delay to wait in ms. 0 means no delay.
*/
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
if (this.sendingBackups) return;
@ -490,8 +480,8 @@ export class BackupManager {
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {number} limit Maximum number of keys to back up
* @returns {number} Number of sessions backed up
* @param limit - Maximum number of keys to back up
* @returns Number of sessions backed up
*/
public async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit);
@ -573,7 +563,7 @@ export class BackupManager {
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
* @returns Promise which resolves to the number of sessions now requiring a backup
* (which will be equal to the number of sessions in the store).
*/
public async flagAllGroupSessionsForBackup(): Promise<number> {
@ -599,7 +589,7 @@ export class BackupManager {
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
* @returns Promise which resolves to the number of sessions requiring backup
*/
public countSessionsNeedingBackup(): Promise<number> {
return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
@ -663,7 +653,7 @@ export class Curve25519 implements BackupAlgorithm {
public get untrusted(): boolean { return true; }
public async encryptSession(data: Record<string, any>): Promise<any> {
public async encryptSession(data: Record<string, any>): Promise<Curve25519SessionData> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
@ -788,7 +778,7 @@ export class Aes256 implements BackupAlgorithm {
public get untrusted(): boolean { return false; }
public encryptSession(data: Record<string, any>): Promise<any> {
public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;

View File

@ -258,7 +258,7 @@ export class DehydrationManager {
}
logger.log("Preparing fallback keys");
const fallbackKeys = {};
const fallbackKeys: Record<string, IOneTimeKey> = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: IOneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k));

View File

@ -16,10 +16,6 @@ limitations under the License.
import { ISignatures } from "../@types/signed";
/**
* @module crypto/deviceinfo
*/
export interface IDevice {
keys: Record<string, string>;
algorithms: string[];
@ -37,69 +33,57 @@ enum DeviceVerification {
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
export class DeviceInfo {
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
* @param obj - raw object from session store
* @param deviceId - id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
* @returns new DeviceInfo
*/
public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
// @ts-ignore - this is messy and typescript doesn't like it
res[prop as keyof IDevice] = obj[prop as keyof IDevice];
}
}
return res;
}
/**
* @enum
*/
public static DeviceVerification = {
VERIFIED: DeviceVerification.Verified,
UNVERIFIED: DeviceVerification.Unverified,
BLOCKED: DeviceVerification.Blocked,
};
/** list of algorithms supported by this device */
public algorithms: string[] = [];
/** a map from `<key type>:<id> -> <base64-encoded key>` */
public keys: Record<string, string> = {};
/** whether the device has been verified/blocked by the user */
public verified = DeviceVerification.Unverified;
/**
* whether the user knows of this device's existence
* (useful when warning the user that a user has added new devices)
*/
public known = false;
/** additional data from the homeserver */
public unsigned: Record<string, any> = {};
public signatures: ISignatures = {};
/**
* @param deviceId - id of the device
*/
public constructor(public readonly deviceId: string) {}
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
* @returns deviceinfo with non-serialised members removed
*/
public toStorage(): IDevice {
return {
@ -115,7 +99,7 @@ export class DeviceInfo {
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
* @returns base64-encoded fingerprint of this device
*/
public getFingerprint(): string {
return this.keys["ed25519:" + this.deviceId];
@ -124,7 +108,7 @@ export class DeviceInfo {
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
* @returns base64-encoded identity key of this device
*/
public getIdentityKey(): string {
return this.keys["curve25519:" + this.deviceId];
@ -133,7 +117,7 @@ export class DeviceInfo {
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
* @returns displayname
*/
public getDisplayName(): string | null {
return this.unsigned.device_display_name || null;
@ -142,7 +126,7 @@ export class DeviceInfo {
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
* @returns true if blocked
*/
public isBlocked(): boolean {
return this.verified == DeviceVerification.Blocked;
@ -151,7 +135,7 @@ export class DeviceInfo {
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
* @returns true if verified
*/
public isVerified(): boolean {
return this.verified == DeviceVerification.Verified;
@ -160,7 +144,7 @@ export class DeviceInfo {
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
* @returns true if unverified
*/
public isUnverified(): boolean {
return this.verified == DeviceVerification.Unverified;
@ -169,7 +153,7 @@ export class DeviceInfo {
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
* @returns true if known
*/
public isKnown(): boolean {
return this.known === true;

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,10 @@ export interface IKeyBackupInfo {
/* eslint-enable camelcase */
export interface IKeyBackupPrepareOpts {
/**
* Whether to use Secure Secret Storage to store the key encrypting key backups.
* Optional, defaults to false.
*/
secureSecretStorage: boolean;
}

View File

@ -15,8 +15,6 @@ limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
@ -31,6 +29,7 @@ import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event";
import { IMessage } from "./algorithms/olm";
enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2",
@ -54,28 +53,26 @@ export const MEGOLM_ALGORITHM = Algorithm.Megolm;
export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
export interface IOlmSessionResult {
/** device info */
device: DeviceInfo;
/** base64 olm session id; null if no session could be established */
sessionId: string | null;
}
/**
* Encrypt an event payload for an Olm device
*
* @param {Object<string, string>} resultsObject The `ciphertext` property
* @param resultsObject - The `ciphertext` property
* of the m.room.encrypted event to which to add our result
*
* @param {string} ourUserId
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
* @param olmDevice - olm.js wrapper
* @param payloadFields - fields to include in the encrypted payload
*
* Returns a promise which resolves (to undefined) when the payload
* has been encrypted into `resultsObject`
*/
export async function encryptMessageForDevice(
resultsObject: Record<string, string>,
resultsObject: Record<string, IMessage>,
ourUserId: string,
ourDeviceId: string | undefined,
olmDevice: OlmDevice,
@ -124,6 +121,7 @@ export async function encryptMessageForDevice(
recipient_keys: {
"ed25519": recipientDevice.getFingerprint(),
},
...payloadFields,
};
// TODO: technically, a bunch of that stuff only needs to be included for
@ -131,11 +129,7 @@ export async function encryptMessageForDevice(
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
Object.assign(payload, payloadFields);
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload));
}
interface IExistingOlmSession {
@ -147,17 +141,14 @@ interface IExistingOlmSession {
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
* @param devicesByUser - map from userid to list of devices to ensure sessions for
*
* @return {Promise} resolves to an array. The first element of the array is a
* @returns resolves to an array. The first element of the array is a
* a map of user IDs to arrays of deviceInfo, representing the devices that
* don't have established olm sessions. The second element of the array is
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
* a map from userId to deviceId to {@link OlmSessionResult}
*/
export async function getExistingOlmSessions(
olmDevice: OlmDevice,
@ -199,27 +190,22 @@ export async function getExistingOlmSessions(
/**
* Try to make sure we have established olm sessions for the given devices.
*
* @param {module:crypto/OlmDevice} olmDevice
* @param devicesByUser - map from userid to list of devices to ensure sessions for
*
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @param {boolean} [force=false] If true, establish a new session even if one
* @param force - If true, establish a new session even if one
* already exists.
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* @param otkTimeout - The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* @param failedServers - An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @param {Logger} [log] A possibly customised log
* @param log - A possibly customised log
*
* @return {Promise} resolves once the sessions are complete, to
* @returns resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
* {@link OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice: OlmDevice,
@ -444,15 +430,15 @@ export interface IObject {
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
* @param olmDevice - olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on.
* @param obj - object to check signature on.
*
* @param {string} signingUserId ID of the user whose signature should be checked
* @param signingUserId - ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
* @param signingDeviceId - ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
* @param signingKey - base64-ed ed25519 public key
*
* Returns a promise which resolves (to undefined) if the the signature is good,
* or rejects with an Error if it is bad.
@ -487,15 +473,15 @@ export async function verifySignature(
/**
* Sign a JSON object using public key cryptography
* @param {Object} obj Object to sign. The object will be modified to include
* @param obj - Object to sign. The object will be modified to include
* the new signature
* @param {Olm.PkSigning|Uint8Array} key the signing object or the private key
* @param key - the signing object or the private key
* seed
* @param {string} userId The user ID who owns the signing key
* @param {string} pubKey The public key (ignored if key is a seed)
* @returns {string} the signature for the object
* @param userId - The user ID who owns the signing key
* @param pubKey - The public key (ignored if key is a seed)
* @returns the signature for the object
*/
export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string {
export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning();
@ -523,9 +509,9 @@ export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: str
/**
* Verify a signed JSON object
* @param {Object} obj Object to verify
* @param {string} pubKey The public key to use to verify
* @param {string} userId The user ID who signed the object
* @param obj - Object to verify
* @param pubKey - The public key to use to verify
* @param userId - The user ID who signed the object
*/
export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
const keyId = "ed25519:" + pubKey;
@ -565,8 +551,8 @@ export function isOlmEncrypted(event: MatrixEvent): boolean {
/**
* Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64");
@ -574,8 +560,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
/**
* Encode a typed array of uint8 as unpadded base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The unpadded base64.
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/=+$/g, '');
@ -583,8 +569,8 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data.
* @param base64 - The base64 to decode.
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
return Buffer.from(base64, "base64");

View File

@ -30,8 +30,6 @@ import { IEncryptedPayload } from "../aes";
/**
* Internal module. Definitions for storage for the crypto module
*
* @module
*/
export interface SecretStorePrivateKeys {
@ -46,8 +44,6 @@ export interface SecretStorePrivateKeys {
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
export interface CryptoStore {
startup(): Promise<CryptoStore>;
@ -69,7 +65,7 @@ export interface CryptoStore {
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string | null) => void);
getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
@ -197,32 +193,29 @@ export interface IWithheld {
/**
* Represents an outgoing room key request
*
* @typedef {Object} OutgoingRoomKeyRequest
*
* @property {string} requestId unique id for this request. Used for both
* an id within the request for later pairing with a cancellation, and for
* the transaction id when sending the to_device messages to our local
* server.
*
* @property {string?} cancellationTxnId
* transaction id for the cancellation, if any
*
* @property {Array<{userId: string, deviceId: string}>} recipients
* list of recipients for the request
*
* @property {module:crypto~RoomKeyRequestBody} requestBody
* parameters for the request.
*
* @property {Number} state current state of this request (states are defined
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
*/
export interface OutgoingRoomKeyRequest {
/**
* Unique id for this request. Used for both an id within the request for later pairing with a cancellation,
* and for the transaction id when sending the to_device messages to our local server.
*/
requestId: string;
requestTxnId?: string;
/**
* Transaction id for the cancellation, if any
*/
cancellationTxnId?: string;
/**
* List of recipients for the request
*/
recipients: IRoomKeyRequestRecipient[];
/**
* Parameters for the request
*/
requestBody: IRoomKeyRequestBody;
/**
* current state of this request (states are defined in {@link OutgoingRoomKeyRequestManager})
*/
state: RoomKeyRequestState;
}

View File

@ -39,14 +39,11 @@ const PROFILE_TRANSACTIONS = false;
* Implementation of a CryptoStore which is backed by an existing
* IndexedDB connection. Generally you want IndexedDBCryptoStore
* which connects to the database and defers to one of these.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class Backend implements CryptoStore {
private nextTxnId = 0;
/**
* @param {IDBDatabase} db
*/
public constructor(private db: IDBDatabase) {
// make sure we close the db on `onversionchange` - otherwise
@ -71,10 +68,9 @@ export class Backend implements CryptoStore {
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* @returns resolves to
* {@link OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
@ -113,11 +109,10 @@ export class Backend implements CryptoStore {
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param requestBody - existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the matching
* {@link OutgoingRoomKeyRequest}, or null if
* not found
*/
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
@ -134,13 +129,12 @@ export class Backend implements CryptoStore {
/**
* look for an existing room key request in the db
*
* @private
* @param {IDBTransaction} txn database transaction
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param {Function} callback function to call with the results of the
* @internal
* @param txn - database transaction
* @param requestBody - existing request to look for
* @param callback - function to call with the results of the
* search. Either passed a matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* {@link OutgoingRoomKeyRequest}, or null if
* not found.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -181,10 +175,10 @@ export class Backend implements CryptoStore {
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
* @param wantedStates - list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the a
* {@link OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
@ -233,8 +227,7 @@ export class Backend implements CryptoStore {
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state
* @returns All elements in a given state
*/
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return new Promise((resolve, reject) => {
@ -294,12 +287,12 @@ export class Backend implements CryptoStore {
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
* @param updates - name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* @returns resolves to
* {@link OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
public updateOutgoingRoomKeyRequest(
@ -337,10 +330,10 @@ export class Backend implements CryptoStore {
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
* @returns resolves once the operation is completed
*/
public deleteOutgoingRoomKeyRequest(
requestId: string,

View File

@ -39,15 +39,11 @@ import { InboundGroupSessionData } from "../OlmDevice";
/**
* Internal module. indexeddb storage for e2e.
*
* @module
*/
/**
* An implementation of CryptoStore, which is normally backed by an indexeddb,
* but with fallback to MemoryCryptoStore.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_ACCOUNT = 'account';
@ -70,8 +66,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
* @param indexedDB - global indexedDB instance
* @param dbName - name of db to connect to
*/
public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
@ -81,7 +77,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* @returns resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
public startup(): Promise<CryptoStore> {
@ -167,7 +163,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Delete all data from this store.
*
* @returns {Promise} resolves when the store has been cleared.
* @returns resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
return new Promise<void>((resolve, reject) => {
@ -206,10 +202,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* @returns resolves to
* {@link OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
@ -219,11 +214,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param requestBody - existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the matching
* {@link OutgoingRoomKeyRequest}, or null if
* not found
*/
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
@ -233,10 +227,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
* @param wantedStates - list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the a
* {@link OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
@ -248,8 +242,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Look for room key requests by state
* unlike above, return a list of all entries in one state.
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state
* @returns Returns an array of requests in the given state
*/
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
@ -258,12 +251,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
* @param {Array<Number>} wantedStates list of acceptable states
* @param userId - Target user ID
* @param deviceId - Target device ID
* @param wantedStates - list of acceptable states
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* @returns resolves to a list of all the
* {@link OutgoingRoomKeyRequest}
*/
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
@ -279,12 +272,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
* @param updates - name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* @returns resolves to
* {@link OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
public updateOutgoingRoomKeyRequest(
@ -301,10 +294,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
* @returns resolves once the operation is completed
*/
public deleteOutgoingRoomKeyRequest(
requestId: string,
@ -319,8 +312,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Get the account pickle from the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
* @param txn - An active transaction. See doTxn().
* @param func - Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
this.backend!.getAccount(txn, func);
@ -330,8 +323,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} accountPickle The new account pickle to store.
* @param txn - An active transaction. See doTxn().
* @param accountPickle - The new account pickle to store.
*/
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend!.storeAccount(txn, accountPickle);
@ -341,9 +334,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Get the public part of the cross-signing keys (eg. self-signing key,
* user signing key).
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
* @param txn - An active transaction. See doTxn().
* @param func - Called with the account keys object:
* `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed
*/
public getCrossSigningKeys(
txn: IDBTransaction,
@ -353,9 +346,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
}
/**
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the private key
* @param {string} type A key type
* @param txn - An active transaction. See doTxn().
* @param func - Called with the private key
* @param type - A key type
*/
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
@ -368,8 +361,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Write the cross-signing keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} keys keys object as getCrossSigningKeys()
* @param txn - An active transaction. See doTxn().
* @param keys - keys object as getCrossSigningKeys()
*/
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this.backend!.storeCrossSigningKeys(txn, keys);
@ -378,9 +371,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Write the cross-signing private keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
* @param txn - An active transaction. See doTxn().
* @param type - The type of cross-signing private key to store
* @param key - keys object as getCrossSigningKeys()
*/
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
@ -394,8 +387,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Returns the number of end-to-end sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions
* @param txn - An active transaction. See doTxn().
* @param func - Called with the count of sessions
*/
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend!.countEndToEndSessions(txn, func);
@ -404,10 +397,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Retrieve a specific end-to-end session between the logged-in user
* and another device.
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID of the session to retrieve
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* @param deviceKey - The public key of the other device.
* @param sessionId - The ID of the session to retrieve
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
@ -425,9 +418,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* @param deviceKey - The public key of the other device.
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
@ -443,8 +436,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Retrieve all end-to-end sessions
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called one for each session with
* @param txn - An active transaction. See doTxn().
* @param func - Called one for each session with
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
@ -454,10 +447,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
* @param deviceKey - The public key of the other device.
* @param sessionId - The ID for this end-to-end session.
* @param sessionInfo - Session information object
* @param txn - An active transaction. See doTxn().
*/
public storeEndToEndSession(
deviceKey: string,
@ -485,10 +478,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Retrieve the end-to-end inbound group session for a given
* server key and session ID
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* @param senderCurve25519Key - The sender's curve 25519 key
* @param sessionId - The ID of the session
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to Base64 end-to-end session.
*/
public getEndToEndInboundGroupSession(
@ -502,10 +495,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Fetches all inbound group sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called once for each group session
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
* @param txn - An active transaction. See doTxn().
* @param func - Called once for each group session
* in the store with an object having keys `{senderKey, sessionId, sessionData}`,
* then once with null to indicate the end of the list.
*/
public getAllEndToEndInboundGroupSessions(
txn: IDBTransaction,
@ -518,10 +511,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Adds an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, the session will not be added.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
* @param senderCurve25519Key - The sender's curve 25519 key
* @param sessionId - The ID of the session
* @param sessionData - The session data structure
* @param txn - An active transaction. See doTxn().
*/
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
@ -536,10 +529,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* Writes an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, it will be overwritten.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
* @param senderCurve25519Key - The sender's curve 25519 key
* @param sessionId - The ID of the session
* @param sessionData - The session data structure
* @param txn - An active transaction. See doTxn().
*/
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
@ -568,8 +561,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* These all need to be written out in full each time such that the snapshot
* is always consistent, so they are stored in one object.
*
* @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn().
* @param txn - An active transaction. See doTxn().
*/
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this.backend!.storeEndToEndDeviceData(deviceData, txn);
@ -578,8 +570,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Get the state of all tracked devices
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the
* @param txn - An active transaction. See doTxn().
* @param func - Function called with the
* device data
*/
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
@ -590,18 +582,18 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
* @param roomId - The room's ID.
* @param roomInfo - The end-to-end info for the room.
* @param txn - An active transaction. See doTxn().
*/
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
* Get an object of roomId->roomInfo for all e2e rooms in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
* Get an object of `roomId->roomInfo` for all e2e rooms in the store
* @param txn - An active transaction. See doTxn().
* @param 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);
@ -611,9 +603,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Get the inbound group sessions that need to be backed up.
* @param {number} limit The maximum number of sessions to retrieve. 0
* @param limit - The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
* @returns resolves to an array of inbound group sessions
*/
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this.backend!.getSessionsNeedingBackup(limit);
@ -621,8 +613,8 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Count the inbound group sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
* @param txn - An active transaction. See doTxn(). (optional)
* @returns resolves to the number of sessions
*/
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this.backend!.countSessionsNeedingBackup(txn);
@ -630,9 +622,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Unmark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
* @param sessions - The sessions that need to be backed up.
* @param txn - An active transaction. See doTxn(). (optional)
* @returns resolves when the sessions are unmarked
*/
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
@ -640,9 +632,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Mark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
* @param sessions - The sessions that need to be backed up.
* @param txn - An active transaction. See doTxn(). (optional)
* @returns resolves when the sessions are marked
*/
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend!.markSessionsNeedingBackup(sessions, txn);
@ -650,10 +642,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Add a shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {string} senderKey The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional)
* @param roomId - The room that the key belongs to
* @param senderKey - The sender's curve 25519 key
* @param sessionId - The ID of the session
* @param txn - An active transaction. See doTxn(). (optional)
*/
public addSharedHistoryInboundGroupSession(
roomId: string,
@ -666,9 +658,9 @@ export class IndexedDBCryptoStore implements CryptoStore {
/**
* Get the shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
* @param roomId - The room that the key belongs to
* @param txn - An active transaction. See doTxn(). (optional)
* @returns Promise which resolves to an array of [senderKey, sessionId]
*/
public getSharedHistoryInboundGroupSessions(
roomId: string,
@ -704,16 +696,16 @@ export class IndexedDBCryptoStore implements CryptoStore {
* only be called within a callback of either this function or
* one of the store functions operating on the same transaction.
*
* @param {string} mode 'readwrite' if you need to call setter
* @param mode - 'readwrite' if you need to call setter
* functions with this transaction. Otherwise, 'readonly'.
* @param {string[]} stores List IndexedDBCryptoStore.STORE_*
* @param stores - List IndexedDBCryptoStore.STORE_*
* options representing all types of object that will be
* accessed or written to with this transaction.
* @param {function(*)} func Function called with the
* @param func - Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @param {Logger} [log] A possibly customised log
* @return {Promise} Promise that resolves with the result of the `func`
* @param log - A possibly customised log
* @returns Promise that resolves with the result of the `func`
* when the transaction is complete. If the backend is
* async (ie. the indexeddb backend) any of the callback
* functions throwing an exception will cause this promise to

View File

@ -28,8 +28,6 @@ import { InboundGroupSessionData } from "../OlmDevice";
* some things backed by localStorage. It exists because indexedDB
* is broken in Firefox private mode or set to, "will not remember
* history".
*
* @module
*/
const E2E_PREFIX = "crypto.";
@ -62,9 +60,6 @@ function keyEndToEndRoomsPrefix(roomId: string): string {
return KEY_ROOMS_PREFIX + roomId;
}
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export class LocalStorageCryptoStore extends MemoryCryptoStore {
public static exists(store: Storage): boolean {
const length = store.length;
@ -338,20 +333,20 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessionsNeedingBackup = getJsonItem<{
[senderKeySessionId: string]: string;
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
return Promise.resolve();
}
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessionsNeedingBackup = getJsonItem<{
[senderKeySessionId: string]: boolean;
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
}
@ -364,7 +359,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
* @returns Promise which resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);

View File

@ -35,13 +35,8 @@ import { InboundGroupSessionData } from "../OlmDevice";
/**
* Internal module. in-memory storage for e2e.
*
* @module
*/
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export class MemoryCryptoStore implements CryptoStore {
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
private account: string | null = null;
@ -65,7 +60,7 @@ export class MemoryCryptoStore implements CryptoStore {
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to the store.
* @returns resolves to the store.
*/
public async startup(): Promise<CryptoStore> {
// No startup work to do for the memory store.
@ -75,7 +70,7 @@ export class MemoryCryptoStore implements CryptoStore {
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
* @returns Promise which resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
return Promise.resolve();
@ -85,10 +80,9 @@ export class MemoryCryptoStore implements CryptoStore {
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* @returns resolves to
* {@link OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
@ -122,11 +116,10 @@ export class MemoryCryptoStore implements CryptoStore {
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param requestBody - existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the matching
* {@link OutgoingRoomKeyRequest}, or null if
* not found
*/
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
@ -138,10 +131,9 @@ export class MemoryCryptoStore implements CryptoStore {
*
* @internal
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param requestBody - existing request to look for
*
* @return {module:crypto/store/base~OutgoingRoomKeyRequest?}
* @returns
* the matching request, or null if not found
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -157,10 +149,10 @@ export class MemoryCryptoStore implements CryptoStore {
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
* @param wantedStates - list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* @returns resolves to the a
* {@link OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states
*/
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
@ -176,8 +168,7 @@ export class MemoryCryptoStore implements CryptoStore {
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
* @returns All OutgoingRoomKeyRequests in state
*/
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return Promise.resolve(
@ -210,12 +201,12 @@ export class MemoryCryptoStore implements CryptoStore {
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
* @param updates - name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* @returns resolves to
* {@link OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
public updateOutgoingRoomKeyRequest(
@ -246,10 +237,10 @@ export class MemoryCryptoStore implements CryptoStore {
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param requestId - ID of request to update
* @param expectedState - state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
* @returns resolves once the operation is completed
*/
public deleteOutgoingRoomKeyRequest(
requestId: string,

View File

@ -17,7 +17,6 @@ limitations under the License.
/**
* Base class for verification methods.
* @module crypto/verification/Base
*/
import { MatrixEvent } from '../../models/event';
@ -74,21 +73,19 @@ export class VerificationBase<
*
* <p>Subclasses must have a NAME class property.</p>
*
* @class
*
* @param {Object} channel the verification channel to send verification messages over.
* @param channel - the verification channel to send verification messages over.
* TODO: Channel types
*
* @param {MatrixClient} baseApis base matrix api interface
* @param baseApis - base matrix api interface
*
* @param {string} userId the user ID that is being verified
* @param userId - the user ID that is being verified
*
* @param {string} deviceId the device ID that is being verified
* @param deviceId - the device ID that is being verified
*
* @param {object} [startEvent] the m.key.verification.start event that
* @param startEvent - the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} [request] the key verification request object related to
* @param request - the key verification request object related to
* this verification, if any
*/
public constructor(
@ -279,7 +276,7 @@ export class VerificationBase<
/**
* Begin the key verification
*
* @returns {Promise} Promise which resolves when the verification has
* @returns Promise which resolves when the verification has
* completed.
*/
public verify(): Promise<void> {

View File

@ -16,8 +16,6 @@ limitations under the License.
/**
* Error messages.
*
* @module crypto/verification/Error
*/
import { MatrixEvent } from "../../models/event";

View File

@ -17,7 +17,6 @@ limitations under the License.
/**
* Verification method that is illegal to have (cannot possibly
* do verification with this method).
* @module crypto/verification/IllegalMethod
*/
import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
@ -26,10 +25,6 @@ import { MatrixClient } from "../../client";
import { MatrixEvent } from "../../models/event";
import { VerificationRequest } from "./request/VerificationRequest";
/**
* @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base}
*/
export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
public static factory(
channel: IVerificationChannel,

View File

@ -16,7 +16,6 @@ limitations under the License.
/**
* QR code key verification.
* @module crypto/verification/QRCode
*/
import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
@ -44,10 +43,6 @@ type EventHandlerMap = {
[QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
} & VerificationEventHandlerMap;
/**
* @class crypto/verification/QRCode/ReciprocateQRCode
* @extends {module:crypto/verification/Base}
*/
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
public reciprocateQREvent?: IReciprocateQr;
@ -283,21 +278,21 @@ export class QRCodeData {
private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b): void => {
const appendByte = (b: number): void => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i): void => {
const appendInt = (i: number): void => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s, enc, withLengthPrefix = true): void => {
const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64): void => {
const appendEncBase64 = (b64: string): void => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
@ -307,7 +302,7 @@ export class QRCodeData {
appendStr(qrData.prefix, "ascii", false);
appendByte(qrData.version);
appendByte(qrData.mode);
appendStr(qrData.transactionId, "utf-8");
appendStr(qrData.transactionId!, "utf-8");
appendEncBase64(qrData.firstKeyB64);
appendEncBase64(qrData.secondKeyB64);
appendEncBase64(qrData.secretB64);

View File

@ -16,7 +16,6 @@ limitations under the License.
/**
* Short Authentication String (SAS) verification.
* @module crypto/verification/SAS
*/
import anotherjson from 'another-json';
@ -140,7 +139,7 @@ function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
const sasGenerators = {
decimal: generateDecimalSas,
emoji: generateEmojiSas,
};
} as const;
export interface IGeneratedSas {
decimal?: [number, number, number];
@ -154,11 +153,12 @@ export interface ISasEvent {
mismatch(): void;
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {};
for (const method of methods) {
if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes);
// @ts-ignore - ts doesn't like us mixing types like this
sas[method] = sasGenerators[method](Array.from(sasBytes));
}
}
return sas;
@ -168,15 +168,14 @@ const macMethods = {
"hkdf-hmac-sha256": "calculate_mac",
"org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
"hmac-sha256": "calculate_mac_long_kdf",
};
} as const;
type Method = keyof typeof macMethods;
type MacMethod = keyof typeof macMethods;
function calculateMAC(olmSAS: OlmSAS, method: Method) {
return function(...args): string {
const macFunction = olmSAS[macMethods[method]];
const mac: string = macFunction.apply(olmSAS, args);
logger.log("SAS calculateMAC:", method, args, mac);
function calculateMAC(olmSAS: OlmSAS, method: MacMethod) {
return function(input: string, info: string): string {
const mac = olmSAS[macMethods[method]](input, info);
logger.log("SAS calculateMAC:", method, [input, info], mac);
return mac;
};
}
@ -202,15 +201,17 @@ const calculateKeyAgreement = {
+ sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
};
} as const;
type KeyAgreement = keyof typeof calculateKeyAgreement;
/* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred
* first).
*/
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
@ -230,10 +231,6 @@ type EventHandlerMap = {
[SasEvent.ShowSas]: (sas: ISasEvent) => void;
} & VerificationEventHandlerMap;
/**
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export class SAS extends Base<SasEvent, EventHandlerMap> {
private waitingForAccept?: boolean;
public ourSASPubKey?: string;
@ -299,10 +296,10 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
}
private async verifyAndCheckMAC(
keyAgreement: string,
keyAgreement: KeyAgreement,
sasMethods: string[],
olmSAS: OlmSAS,
macMethod: Method,
macMethod: MacMethod,
): Promise<void> {
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => {
@ -354,7 +351,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
throw new SwitchStartEventError(this.startEvent);
}
let e;
let e: MatrixEvent;
try {
e = await this.waitForEvent(EventType.KeyVerificationAccept);
} finally {
@ -445,8 +442,8 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
}
}
private sendMAC(olmSAS: OlmSAS, method: Method): Promise<void> {
const mac = {};
private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise<void> {
const mac: Record<string, string> = {};
const keyList: string[] = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.baseApis.getUserId() + this.baseApis.deviceId
@ -455,7 +452,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this.baseApis.getDeviceEd25519Key(),
this.baseApis.getDeviceEd25519Key()!,
baseInfo + deviceKeyId,
);
keyList.push(deviceKeyId);
@ -477,7 +474,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
return this.send(EventType.KeyVerificationMac, { mac, keys });
}
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise<void> {
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise<void> {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this.baseApis.getUserId() + this.baseApis.deviceId

View File

@ -17,11 +17,11 @@ 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
* @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 |
* +--------+--------+--------+--------+--------+

View File

@ -40,9 +40,9 @@ export class InRoomChannel implements IVerificationChannel {
private requestEventId?: string;
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
* @param client - the matrix client, to send messages with and get current user & device from.
* @param roomId - id of the room where verification events should be posted in, should be a DM with the given user.
* @param userId - id of user that the verification request is directed at, should be present in the room.
*/
public constructor(
private readonly client: MatrixClient,
@ -78,8 +78,8 @@ export class InRoomChannel implements IVerificationChannel {
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
* @param event - the event to get the timestamp of
* @returns the timestamp when the event was sent
*/
public getTimestamp(event: MatrixEvent): number {
return event.getTs();
@ -87,8 +87,8 @@ export class InRoomChannel implements IVerificationChannel {
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {boolean} boolean flag
* @param type - the event type to check
* @returns boolean flag
*/
public static canCreateRequest(type: string): boolean {
return type === REQUEST_TYPE;
@ -100,8 +100,8 @@ export class InRoomChannel implements IVerificationChannel {
/**
* Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
* @param event - the event
* @returns the transaction id
*/
public static getTransactionId(event: MatrixEvent): string | undefined {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
@ -119,9 +119,9 @@ export class InRoomChannel implements IVerificationChannel {
* This only does checks that don't rely on the current state of a potentially already channel
* so we can prevent channels being created by invalid events.
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {boolean} whether the event is valid and should be passed to handleEvent
* @param event - the event to validate
* @param client - the client to get the current user and device id from
* @returns whether the event is valid and should be passed to handleEvent
*/
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
const txnId = InRoomChannel.getTransactionId(event);
@ -156,8 +156,8 @@ export class InRoomChannel implements IVerificationChannel {
* As m.key.verification.request events are as m.room.message events with the InRoomChannel
* to have a fallback message in non-supporting clients, we map the real event type
* to the symbolic one to keep things in unison with ToDeviceChannel
* @param {MatrixEvent} event the event to get the type of
* @returns {string} the "symbolic" event type
* @param event - the event to get the type of
* @returns the "symbolic" event type
*/
public static getEventType(event: MatrixEvent): string {
const type = event.getType();
@ -179,10 +179,10 @@ export class InRoomChannel implements IVerificationChannel {
/**
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @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.
* @param event - to handle
* @param request - the request to forward handling to
* @param isLiveEvent - whether this is an even received through sync or not
* @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
// prevent processing the same event multiple times, as under
@ -228,8 +228,8 @@ export class InRoomChannel implements IVerificationChannel {
* so it has the same format as returned by `completeContent` before sending.
* The relation can not appear on the event content because of encryption,
* relations are excluded from encryption.
* @param {MatrixEvent} event the received event
* @returns {Object} the content object with the relation added again
* @param event - the received event
* @returns the content object with the relation added again
*/
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
// ensure m.related_to is included in e2ee rooms
@ -244,9 +244,9 @@ export class InRoomChannel implements IVerificationChannel {
* This is public so verification methods (SAS uses this) can get the exact
* content that will be sent independent of the used channel,
* as they need to calculate the hash of it.
* @param {string} type the event type
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
* @param type - the event type
* @param content - the (incomplete) content
* @returns the complete content, as it will be sent.
*/
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
content = Object.assign({}, content);
@ -276,9 +276,9 @@ export class InRoomChannel implements IVerificationChannel {
/**
* Send an event over the channel with the content not having gone through `completeContent`.
* @param {string} type the event type
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
* @param type - the event type
* @param uncompletedContent - the (incomplete) content
* @returns the promise of the request
*/
public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
const content = this.completeContent(type, uncompletedContent);
@ -287,9 +287,8 @@ export class InRoomChannel implements IVerificationChannel {
/**
* Send an event over the channel with the content having gone through `completeContent` already.
* @param {string} type the event type
* @param {object} content
* @returns {Promise} the promise of the request
* @param type - the event type
* @returns the promise of the request
*/
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let sendType = type;

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