Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fbcef556e | |||
| b68ad00394 | |||
| 836238c3ba | |||
| 014b29b303 | |||
| 74160806c0 | |||
| 8e0ef98bcc | |||
| d7831f9e5b | |||
| 989c5a3dda | |||
| 0778c4e01e | |||
| c65e329101 | |||
| 5ddd453699 | |||
| 42d982dd69 | |||
| f406ffd3dd | |||
| dec4650d3d | |||
| 4c00b41046 | |||
| a1845ba0ff | |||
| fb9e258468 | |||
| 974723ceef | |||
| 5788d9744b | |||
| 65cbbaaf01 | |||
| c5245a887b | |||
| 321679fd63 | |||
| 15c679b29e | |||
| 85ba069117 | |||
| 9b8dcf53ed | |||
| 324af3ee67 | |||
| ec6c0946d4 | |||
| e5f480b032 | |||
| 6bf4ed8672 | |||
| c18d691ef5 | |||
| 97cf73bc52 | |||
| aa25103665 | |||
| 858db67778 | |||
| e230abee45 | |||
| 8c16d69f3c | |||
| 55b9116c99 | |||
| 3a5d66057e | |||
| 3f7af189e4 | |||
| 16ddcb0ed0 | |||
| 9e35b8dd0a | |||
| bed787b749 | |||
| d260b8be56 | |||
| 97991dad02 | |||
| b8c19c47ab |
@@ -23,7 +23,7 @@ jobs:
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2
|
||||
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
|
||||
@@ -15,7 +15,7 @@ concurrency:
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.76.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.78.0
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.76.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.78.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
+36
-1
@@ -1,6 +1,41 @@
|
||||
Changes in [27.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0-rc.1) (2023-08-08)
|
||||
Changes in [28.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.1.0-rc.1) (2023-09-05)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate `MatrixClient.checkUserTrust` ([\#3691](https://github.com/matrix-org/matrix-js-sdk/pull/3691)).
|
||||
* Deprecate `MatrixClient.{prepare,create}KeyBackupVersion` in favour of new `CryptoApi.resetKeyBackup` API ([\#3689](https://github.com/matrix-org/matrix-js-sdk/pull/3689)).
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* Allow calls without ICE/TURN/STUN servers ([\#3695](https://github.com/matrix-org/matrix-js-sdk/pull/3695)).
|
||||
* Emit summary update event ([\#3687](https://github.com/matrix-org/matrix-js-sdk/pull/3687)). Fixes vector-im/element-web#26033.
|
||||
* ElementR: Update `CryptoApi.userHasCrossSigningKeys` ([\#3646](https://github.com/matrix-org/matrix-js-sdk/pull/3646)). Contributed by @florianduros.
|
||||
* Add `join_rule` field to /publicRooms response ([\#3673](https://github.com/matrix-org/matrix-js-sdk/pull/3673)). Contributed by @charlynguyen.
|
||||
* Use sender instead of content.creator field on m.room.create events ([\#3675](https://github.com/matrix-org/matrix-js-sdk/pull/3675)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Provide better error for ICE Server SyntaxError ([\#3694](https://github.com/matrix-org/matrix-js-sdk/pull/3694)). Fixes vector-im/element-web#21804.
|
||||
* Legacy crypto: re-check key backup after `bootstrapSecretStorage` ([\#3692](https://github.com/matrix-org/matrix-js-sdk/pull/3692)). Fixes vector-im/element-web#26115.
|
||||
|
||||
Changes in [28.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.0.0) (2023-08-29)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Set minimum supported Matrix 1.1 version (drop legacy r0 versions) ([\#3007](https://github.com/matrix-org/matrix-js-sdk/pull/3007)). Fixes vector-im/element-web#16876.
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
## ✨ Features
|
||||
* ElementR: Add `CryptoApi.requestVerificationDM` ([\#3643](https://github.com/matrix-org/matrix-js-sdk/pull/3643)). Contributed by @florianduros.
|
||||
* Implement `CryptoApi.checkKeyBackupAndEnable` ([\#3633](https://github.com/matrix-org/matrix-js-sdk/pull/3633)). Fixes vector-im/crypto-internal#111 and vector-im/crypto-internal#112.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* ElementR: Process all verification events, not just requests ([\#3650](https://github.com/matrix-org/matrix-js-sdk/pull/3650)). Contributed by @florianduros.
|
||||
|
||||
Changes in [27.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0) (2023-08-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
|
||||
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "27.2.0-rc.1",
|
||||
"version": "28.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -55,7 +55,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.1",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -101,9 +101,9 @@
|
||||
"browserify-swap": "^0.2.2",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.45.0",
|
||||
"eslint": "8.47.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
logger.log(this + ": starting");
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||
// we have tests that rely on support for lazy-loading members
|
||||
versions: ["r0.5.0"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
@@ -25,17 +25,28 @@ import Olm from "@matrix-org/olm";
|
||||
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID, TEST_ROOM_ID as ROOM_ID, TEST_USER_ID } from "../../test-utils/test-data";
|
||||
import {
|
||||
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
BOB_SIGNED_TEST_DEVICE_DATA,
|
||||
BOB_TEST_USER_ID,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
TEST_ROOM_ID,
|
||||
TEST_ROOM_ID as ROOM_ID,
|
||||
TEST_USER_ID,
|
||||
} from "../../test-utils/test-data";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
Category,
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
IClaimOTKsResult,
|
||||
IContent,
|
||||
IDownloadKeyResult,
|
||||
IEvent,
|
||||
IndexedDBCryptoStore,
|
||||
IRoomEvent,
|
||||
IStartClientOpts,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
@@ -44,7 +55,6 @@ import {
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomStateEvent,
|
||||
IRoomEvent,
|
||||
} from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
@@ -52,9 +62,13 @@ import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../../src/utils";
|
||||
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import {
|
||||
mockInitialApiRequests,
|
||||
mockSetupCrossSigningRequests,
|
||||
mockSetupMegolmBackupRequests,
|
||||
} from "../../test-utils/mockEndpoints";
|
||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { CryptoCallbacks } from "../../../src/crypto-api";
|
||||
import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1945,7 +1959,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
};
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/r0/keys/upload", "/_matrix/client/v3/keys/upload"]) {
|
||||
for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, {
|
||||
// These routes are already defined in the E2EKeyReceiver
|
||||
// We want to overwrite the behaviour of the E2EKeyReceiver
|
||||
@@ -2082,9 +2096,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
return queryResponseBody;
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/r0/keys/query", "/_matrix/client/v3/keys/query"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener);
|
||||
}
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
||||
listener,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2175,7 +2190,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)`
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)`
|
||||
* Resolved when a key is uploaded (ie in `body.content.key`)
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
@@ -2184,14 +2199,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// This url is called multiple times during the secret storage bootstrap process
|
||||
// When we received the newly generated key, we return it
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
if (content.key) {
|
||||
resolve(content.key);
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
@@ -2200,7 +2213,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/m.cross_signing.${key}`
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`
|
||||
* Resolved when the cross signing key is uploaded
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
@@ -2208,7 +2221,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/r0/user/:userId/account_data/m.cross_signing.${key}`,
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
resolve(content.encrypted);
|
||||
@@ -2218,6 +2231,73 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
}
|
||||
|
||||
function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the megolm backup key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all mocks needed to setup cross-signing, key backup, 4S and then
|
||||
* configure the account to have recovery.
|
||||
*
|
||||
* @param backupVersion - The version of the created backup
|
||||
*/
|
||||
async function bootstrapSecurity(backupVersion: string): Promise<void> {
|
||||
mockSetupCrossSigningRequests();
|
||||
mockSetupMegolmBackupRequests(backupVersion);
|
||||
|
||||
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
|
||||
const backupStatusUpdate = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const setupPromises = [
|
||||
awaitCrossSigningKeyUpload("master"),
|
||||
awaitCrossSigningKeyUpload("user_signing"),
|
||||
awaitCrossSigningKeyUpload("self_signing"),
|
||||
awaitMegolmBackupKeyUpload(),
|
||||
];
|
||||
|
||||
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
|
||||
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
|
||||
|
||||
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
|
||||
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({
|
||||
setupNewSecretStorage: true,
|
||||
createSecretStorageKey,
|
||||
setupNewKeyBackup: true,
|
||||
});
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
await Promise.all(setupPromises);
|
||||
|
||||
// wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
// Finally ensure backup is working
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
await backupStatusUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send in the sync response the provided `secretStorageKey` into the account_data field
|
||||
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
|
||||
@@ -2265,7 +2345,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly("should create a new key", async () => {
|
||||
it("should create a new key", async () => {
|
||||
const bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
@@ -2308,46 +2388,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage",
|
||||
async () => {
|
||||
let bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
|
||||
let bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
// Wait for the key to be uploaded in the account data
|
||||
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// Call again bootstrapSecretStorage
|
||||
bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
// Call again bootstrapSecretStorage
|
||||
bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
// Wait for the key to be uploaded in the account data
|
||||
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
newBackendOnly("should upload cross signing keys", async () => {
|
||||
it("should upload cross signing keys", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
|
||||
await aliceClient.getCrypto()?.bootstrapCrossSigning({});
|
||||
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
|
||||
|
||||
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
|
||||
const bootstrapPromise = aliceClient
|
||||
@@ -2375,6 +2452,104 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(userSigningKey[secretStorageKey]).toBeDefined();
|
||||
expect(selfSigningKey[secretStorageKey]).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a new megolm backup", async () => {
|
||||
const backupVersion = "abc";
|
||||
await bootstrapSecurity(backupVersion);
|
||||
|
||||
// Expect a backup to be available and used
|
||||
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||
expect(activeBackup).toStrictEqual(backupVersion);
|
||||
|
||||
// check that there is a MSK signature
|
||||
const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data!
|
||||
.signatures;
|
||||
expect(signatures).toBeDefined();
|
||||
expect(signatures![aliceClient.getUserId()!]).toBeDefined();
|
||||
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
|
||||
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
|
||||
});
|
||||
|
||||
it("Reset key backup should create a new backup and update 4S", async () => {
|
||||
// First set up 4S and key backup
|
||||
const backupVersion = "1";
|
||||
await bootstrapSecurity(backupVersion);
|
||||
|
||||
const currentVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||
const currentBackupKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
|
||||
// we will call reset backup, it should delete the existing one, then setup a new one
|
||||
// Let's mock for that
|
||||
|
||||
// Mock delete and replace the GET to return 404 as soon as called
|
||||
const awaitDeleteCalled = new Promise<void>((resolve) => {
|
||||
fetchMock.delete(
|
||||
"express:/_matrix/client/v3/room_keys/version/:version",
|
||||
(url: string, options: RequestInit) => {
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
resolve();
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
const newVersion = "2";
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
|
||||
backupData.version = newVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
|
||||
// update get call with new version
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const newBackupStatusUpdate = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const newBackupUploadPromise = awaitMegolmBackupKeyUpload();
|
||||
|
||||
await aliceClient.getCrypto()!.resetKeyBackup();
|
||||
await awaitDeleteCalled;
|
||||
await newBackupStatusUpdate;
|
||||
await newBackupUploadPromise;
|
||||
|
||||
const nextVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||
const nextKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
|
||||
expect(nextVersion).toBeDefined();
|
||||
expect(nextVersion).not.toEqual(currentVersion);
|
||||
expect(nextKey).not.toEqual(currentBackupKey);
|
||||
|
||||
// The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend
|
||||
// ensure that it works anyhow
|
||||
await aliceClient.deleteKeyBackupVersion(nextVersion!);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
// XXX Legacy crypto does not update 4S when doing that; should ensure that rust implem does it.
|
||||
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Incoming verification in a DM", () => {
|
||||
@@ -2565,4 +2740,46 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Check if the cross signing keys are available for a user", () => {
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
||||
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
});
|
||||
|
||||
it("Cross signing keys are available for an untracked user with cross signing keys on the homeserver", async () => {
|
||||
// Needed for old crypto, download and cache locally the cross signing keys of Bob
|
||||
await aliceClient.getCrypto()?.getUserDeviceInfo([BOB_TEST_USER_ID], true);
|
||||
|
||||
const hasCrossSigningKeysForUser = await aliceClient
|
||||
.getCrypto()!
|
||||
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
});
|
||||
|
||||
it("Cross signing keys are available for a tracked user", async () => {
|
||||
// Process Alice keys, old crypto has a sleep(5ms) during the process
|
||||
await jest.advanceTimersByTimeAsync(5);
|
||||
await flushPromises();
|
||||
|
||||
// Alice is the local user and should be tracked !
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
});
|
||||
|
||||
it("Cross signing keys are not available for an unknown user", async () => {
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, IEvent, MatrixClient, TypedEventEmitter } from "../../../src";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
@@ -27,6 +27,7 @@ import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
@@ -83,6 +84,58 @@ afterEach(() => {
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
enum MockKeyUploadEvent {
|
||||
KeyUploaded = "KeyUploaded",
|
||||
}
|
||||
|
||||
type MockKeyUploadEventHandlerMap = {
|
||||
[MockKeyUploadEvent.KeyUploaded]: (roomId: string, sessionId: string, backupVersion: string) => void;
|
||||
};
|
||||
|
||||
/*
|
||||
* Test helper. Returns an event emitter that will emit an event every time fetchmock sees a request to backup a key.
|
||||
*/
|
||||
function mockUploadEmitter(
|
||||
expectedVersion: string,
|
||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||
const emitter = new TypedEventEmitter();
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
if (version != expectedVersion) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
current_version: expectedVersion,
|
||||
errcode: "M_WRONG_ROOM_KEYS_VERSION",
|
||||
error: "Wrong backup version.",
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
emitter.emit(MockKeyUploadEvent.KeyUploaded, roomId, sessionId, version);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: count,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
@@ -176,7 +229,256 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(event.getContent()).toEqual("testytest");
|
||||
});
|
||||
|
||||
oldBackendOnly("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that signalling is working
|
||||
const remainingZeroPromise = new Promise<void>((resolve, reject) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
const uploadMockEmitter = mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
|
||||
|
||||
const uploadPromises = someRoomKeys.map((data) => {
|
||||
new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (
|
||||
data.room_id == roomId &&
|
||||
data.session_id == sessionId &&
|
||||
version == testData.SIGNED_BACKUP_DATA.version
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
// Wait until all keys are backed up to ensure that when a new key is received the loop is restarted
|
||||
await remainingZeroPromise;
|
||||
|
||||
// A new key import should trigger a new upload.
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const newKeyUploadPromise = new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (
|
||||
newKey.room_id == roomId &&
|
||||
newKey.session_id == sessionId &&
|
||||
version == testData.SIGNED_BACKUP_DATA.version
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
it("Alice should re-upload all keys if a new trusted backup is available", async function () {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that signalling is working
|
||||
const remainingZeroPromise = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
|
||||
// wait for all keys to be backed up
|
||||
await remainingZeroPromise;
|
||||
|
||||
const newBackupVersion = "2";
|
||||
const uploadMockEmitter = mockUploadEmitter(newBackupVersion);
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
// Let's simulate that a new backup is available by returning error code on key upload
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// If we import a new key the loop will try to upload to old version, it will
|
||||
// fail then check the current version and switch if trusted
|
||||
const uploadPromises = someRoomKeys.map((data) => {
|
||||
new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (data.room_id == roomId && data.session_id == sessionId && version == newBackupVersion) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const disableOldBackup = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupFailed, (errCode) => {
|
||||
if (errCode == "M_WRONG_ROOM_KEYS_VERSION") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const enableNewBackup = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// A new key import should trigger a new upload.
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const newKeyUploadPromise = new Promise<void>((resolve) => {
|
||||
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
|
||||
if (newKey.room_id == roomId && newKey.session_id == sessionId && version == newBackupVersion) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await disableOldBackup;
|
||||
await enableNewBackup;
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
it("Backup loop should be resistant to network failures", async function () {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
// on the first key upload attempt, simulate a network failure
|
||||
const failurePromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// kick the import loop off and wait for the failed request
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.runAllTimers();
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
const successPromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
||||
const allKeysUploadedPromise = new Promise((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
|
||||
if (remaining == 0) {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.runAllTimersAsync();
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
});
|
||||
|
||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
@@ -187,7 +489,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.checkKeyBackup();
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
// At this point there is no backup
|
||||
let backupStatus: string | null;
|
||||
@@ -201,9 +503,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
const checked = await aliceClient.checkKeyBackup();
|
||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
expect(checked?.trustInfo?.usable).toBeFalsy();
|
||||
expect(checked?.trustInfo?.trusted).toBeFalsy();
|
||||
|
||||
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
|
||||
expect(backupStatus).toBeNull();
|
||||
@@ -222,8 +524,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
});
|
||||
|
||||
const validCheck = await aliceClient.checkKeyBackup();
|
||||
expect(validCheck?.trustInfo?.usable).toStrictEqual(true);
|
||||
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
|
||||
|
||||
await backupPromise;
|
||||
|
||||
@@ -286,6 +588,128 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkKeyBackupAndEnable", () => {
|
||||
it("enables a backup signed by a trusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
it("does not enable a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// download the device list, to match the trusted case
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
|
||||
it("disables backup when a new untrusted backup is available", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
unsignedBackup.version = "2";
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
|
||||
it("switches backup when a new trusted backup is available", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
const newBackupVersion = "2";
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||
});
|
||||
|
||||
it("Disables when backup is deleted", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(noResult).toBeNull();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/** make sure that the client knows about the dummy device */
|
||||
async function waitForDeviceList(): Promise<void> {
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
|
||||
@@ -22,7 +22,15 @@ import fetchMock from "fetch-mock-jest";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
|
||||
import {
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
IContent,
|
||||
ICreateClientOpts,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../../src";
|
||||
import {
|
||||
canAcceptVerificationRequest,
|
||||
ShowQrCodeCallbacks,
|
||||
@@ -34,15 +42,20 @@ import {
|
||||
VerifierEvent,
|
||||
} from "../../../src/crypto-api/verification";
|
||||
import { escapeRegExp } from "../../../src/utils";
|
||||
import { CRYPTO_BACKENDS, emitPromise, InitCrypto } from "../../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
BOB_SIGNED_TEST_DEVICE_DATA,
|
||||
BOB_TEST_USER_ID,
|
||||
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
TEST_DEVICE_ID,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
TEST_ROOM_ID,
|
||||
TEST_USER_ID,
|
||||
BOB_ONE_TIME_KEYS,
|
||||
} from "../../test-utils/test-data";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
@@ -808,6 +821,82 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
});
|
||||
|
||||
describe("Send verification request in DM", () => {
|
||||
beforeEach(async () => {
|
||||
aliceClient = await startTestClient();
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
|
||||
e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
|
||||
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a mock to respond when the verification request is sent
|
||||
* Handle both encrypted and unencrypted requests
|
||||
*/
|
||||
function awaitRoomMessageRequest(): Promise<IContent> {
|
||||
return new Promise((resolve) => {
|
||||
// Case of unencrypted message of the new crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
|
||||
(url: string, options: RequestInit) => {
|
||||
resolve(JSON.parse(options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
},
|
||||
);
|
||||
|
||||
// Case of encrypted message of the old crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
||||
async (url: string, options: RequestInit) => {
|
||||
const encryptedMessage = JSON.parse(options.body as string);
|
||||
const event = new MatrixEvent({
|
||||
content: encryptedMessage,
|
||||
type: "m.room.encrypted",
|
||||
room_id: TEST_ROOM_ID,
|
||||
});
|
||||
// Try to decrypt the event
|
||||
event.once(MatrixEventEvent.Decrypted, (decryptedEvent: MatrixEvent, error?: Error) => {
|
||||
expect(error).not.toBeDefined();
|
||||
resolve(decryptedEvent.getContent());
|
||||
});
|
||||
await aliceClient.decryptEventIfNeeded(event);
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("alice sends a verification request in a DM to bob", async () => {
|
||||
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({ one_time_keys: BOB_ONE_TIME_KEYS }));
|
||||
|
||||
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
||||
// 5ms by users, ie Bob and Alice
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
const messageRequestPromise = awaitRoomMessageRequest();
|
||||
const verificationRequest = await aliceClient
|
||||
.getCrypto()!
|
||||
.requestVerificationDM(BOB_TEST_USER_ID, TEST_ROOM_ID);
|
||||
const requestContent = await messageRequestPromise;
|
||||
|
||||
expect(requestContent.from_device).toBe(aliceClient.getDeviceId());
|
||||
expect(requestContent.methods.sort()).toStrictEqual(
|
||||
["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"].sort(),
|
||||
);
|
||||
expect(requestContent.msgtype).toBe("m.key.verification.request");
|
||||
expect(requestContent.to).toBe(BOB_TEST_USER_ID);
|
||||
|
||||
expect(verificationRequest.roomId).toBe(TEST_ROOM_ID);
|
||||
expect(verificationRequest.isSelfVerification).toBe(false);
|
||||
expect(verificationRequest.otherUserId).toBe(BOB_TEST_USER_ID);
|
||||
});
|
||||
});
|
||||
|
||||
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
const client = createClient({
|
||||
baseUrl: TEST_HOMESERVER_URL,
|
||||
|
||||
@@ -92,9 +92,7 @@ describe("MatrixClient events", function () {
|
||||
type: "m.room.create",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar",
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -107,9 +107,7 @@ const INITIAL_SYNC_DATA = {
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
content: {},
|
||||
event: false,
|
||||
}),
|
||||
],
|
||||
@@ -1342,7 +1340,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/context/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: event.event_id!,
|
||||
}),
|
||||
@@ -1360,7 +1358,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/event/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: event.event_id!,
|
||||
}),
|
||||
@@ -1371,7 +1369,7 @@ describe("MatrixClient event timelines", function () {
|
||||
function respondToMessagesRequest(): ExpectedHttpRequest {
|
||||
const request = httpBackend.when(
|
||||
"GET",
|
||||
encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
|
||||
encodeUri("/_matrix/client/v3/rooms/$roomId/messages", {
|
||||
$roomId: roomId,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
it("should upload the file", function () {
|
||||
httpBackend
|
||||
.when("POST", "/_matrix/media/r0/upload")
|
||||
.when("POST", "/_matrix/media/v3/upload")
|
||||
.check(function (req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.queryParams?.filename).toEqual("hi.txt");
|
||||
@@ -108,7 +108,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
it("should parse errors into a MatrixError", function () {
|
||||
httpBackend
|
||||
.when("POST", "/_matrix/media/r0/upload")
|
||||
.when("POST", "/_matrix/media/v3/upload")
|
||||
.check(function (req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
// @ts-ignore private property
|
||||
@@ -708,7 +708,7 @@ describe("MatrixClient", function () {
|
||||
const auth = { identifier: 1 };
|
||||
it("should pass through an auth dict", function () {
|
||||
httpBackend
|
||||
.when("DELETE", "/_matrix/client/r0/devices/my_device")
|
||||
.when("DELETE", "/_matrix/client/v3/devices/my_device")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({ auth: auth });
|
||||
})
|
||||
@@ -1102,10 +1102,6 @@ describe("MatrixClient", function () {
|
||||
submit_url: "https://foobar.matrix/_matrix/matrix",
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.6.0"],
|
||||
});
|
||||
|
||||
const prom = client.requestRegisterEmailToken("bob@email", "secret", 1);
|
||||
httpBackend
|
||||
.when("POST", "/register/email/requestToken")
|
||||
@@ -1126,10 +1122,6 @@ describe("MatrixClient", function () {
|
||||
it("should supply an id_access_token", async () => {
|
||||
const targetEmail = "gerald@example.org";
|
||||
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.6.0"],
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/invite")
|
||||
.check((req) => {
|
||||
@@ -1165,10 +1157,6 @@ describe("MatrixClient", function () {
|
||||
],
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.6.0"],
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/createRoom")
|
||||
.check((req) => {
|
||||
@@ -1652,6 +1640,82 @@ describe("MatrixClient", function () {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFallbackAuthUrl", () => {
|
||||
it("should return fallback url", () => {
|
||||
expect(client.getFallbackAuthUrl("loginType", "authSessionId")).toMatchInlineSnapshot(
|
||||
`"http://alice.localhost.test.server/_matrix/client/v3/auth/loginType/fallback/web?session=authSessionId"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addThreePidOnly", () => {
|
||||
it("should make expected POST request", async () => {
|
||||
httpBackend
|
||||
.when("POST", "/_matrix/client/v3/account/3pid/add")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({
|
||||
client_secret: "secret",
|
||||
sid: "sid",
|
||||
});
|
||||
expect(req.headers["Authorization"]).toBe("Bearer " + accessToken);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await Promise.all([
|
||||
client.addThreePidOnly({
|
||||
client_secret: "secret",
|
||||
sid: "sid",
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindThreePid", () => {
|
||||
it("should make expected POST request", async () => {
|
||||
httpBackend
|
||||
.when("POST", "/_matrix/client/v3/account/3pid/bind")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({
|
||||
client_secret: "secret",
|
||||
id_server: "server",
|
||||
id_access_token: "token",
|
||||
sid: "sid",
|
||||
});
|
||||
expect(req.headers["Authorization"]).toBe("Bearer " + accessToken);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await Promise.all([
|
||||
client.bindThreePid({
|
||||
client_secret: "secret",
|
||||
id_server: "server",
|
||||
id_access_token: "token",
|
||||
sid: "sid",
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unbindThreePid", () => {
|
||||
it("should make expected POST request", async () => {
|
||||
httpBackend
|
||||
.when("POST", "/_matrix/client/v3/account/3pid/unbind")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({
|
||||
medium: "email",
|
||||
address: "alice@server.com",
|
||||
id_server: "identity.localhost",
|
||||
});
|
||||
expect(req.headers["Authorization"]).toBe("Bearer " + accessToken);
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
@@ -1925,7 +1989,6 @@ const buildEventCreate = () =>
|
||||
new MatrixEvent({
|
||||
age: 80126105,
|
||||
content: {
|
||||
creator: "@andybalaam-test1:matrix.org",
|
||||
room_version: "6",
|
||||
},
|
||||
event_id: "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
|
||||
|
||||
@@ -57,9 +57,7 @@ describe("MatrixClient opts", function () {
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -85,9 +85,7 @@ describe("MatrixClient room timelines", function () {
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -224,8 +224,6 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should honour lazyLoadMembers if user is not a guest", () => {
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
@@ -242,8 +240,6 @@ describe("MatrixClient syncing", () => {
|
||||
it("should not honour lazyLoadMembers if user is a guest", () => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
|
||||
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
@@ -393,9 +389,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -581,9 +575,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -615,9 +607,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomTwo,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -762,7 +752,6 @@ describe("MatrixClient syncing", () => {
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
content: {
|
||||
creator: otherUserId,
|
||||
room_version: "9",
|
||||
},
|
||||
});
|
||||
@@ -848,7 +837,6 @@ describe("MatrixClient syncing", () => {
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
content: {
|
||||
creator: otherUserId,
|
||||
room_version: testMeta.roomVersion,
|
||||
},
|
||||
});
|
||||
@@ -1376,9 +1364,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
} as Partial<IJoinedRoom>,
|
||||
@@ -1475,9 +1461,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -1633,9 +1617,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -179,7 +179,6 @@ describe("MatrixClient syncing", () => {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
creator: userB,
|
||||
room_version: "9",
|
||||
},
|
||||
origin_server_ts: 1,
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("SlidingSyncSdk", () => {
|
||||
await client!.initCrypto();
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
};
|
||||
|
||||
@@ -188,7 +188,7 @@ describe("SlidingSyncSdk", () => {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
|
||||
@@ -203,7 +203,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "B",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
|
||||
@@ -215,7 +215,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "C",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
|
||||
@@ -228,7 +228,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "D",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
|
||||
@@ -264,7 +264,7 @@ describe("SlidingSyncSdk", () => {
|
||||
[roomF]: {
|
||||
name: "#foo:localhost",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
|
||||
@@ -280,7 +280,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "G",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
@@ -292,7 +292,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "H",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
|
||||
@@ -602,7 +602,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
@@ -718,7 +718,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with account data",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
@@ -922,7 +922,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
@@ -963,7 +963,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
@@ -1049,7 +1049,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "Room with receipts",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
|
||||
@@ -75,8 +75,6 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
// catch both r0 and v3 variants
|
||||
fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,8 +43,6 @@ export class E2EKeyResponder {
|
||||
public constructor(homeserverUrl: string) {
|
||||
// set up a listener for /keys/query.
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
|
||||
// catch both r0 and v3 variants
|
||||
fetchMock.post(new URL("/_matrix/client/r0/keys/query", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export class SyncResponder implements ISyncResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ export const mockClientMethodsEvents = () => ({
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
|
||||
@@ -16,15 +16,17 @@ limitations under the License.
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { KeyBackupInfo } from "../../src/crypto-api";
|
||||
|
||||
/**
|
||||
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
|
||||
*
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
}
|
||||
@@ -32,13 +34,13 @@ export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
*
|
||||
* Return 404 error for `GET _matrix/client/r0/user/:userId/account_data/:type` request
|
||||
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
export function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {
|
||||
fetchMock.get("express:/_matrix/client/v3/user/:userId/account_data/:type", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
});
|
||||
@@ -56,3 +58,35 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock out requests to `/room_keys/version`.
|
||||
*
|
||||
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
|
||||
* Once the POST is done, `GET /room_keys/version` will return the posted backup
|
||||
* instead of 404.
|
||||
*
|
||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||
*/
|
||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,74 +30,122 @@ import json
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from random import randbytes, seed
|
||||
|
||||
# input data
|
||||
TEST_USER_ID = "@alice:localhost"
|
||||
TEST_DEVICE_ID = "test_device"
|
||||
TEST_ROOM_ID = "!room:id"
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
||||
ALICE_DATA = {
|
||||
"TEST_USER_ID": "@alice:localhost",
|
||||
"TEST_DEVICE_ID": "test_device",
|
||||
"TEST_ROOM_ID": "!room:id",
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef",
|
||||
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
|
||||
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale",
|
||||
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser",
|
||||
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself",
|
||||
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
B64_BACKUP_DECRYPTION_KEY = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
"B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
|
||||
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
|
||||
}
|
||||
|
||||
BOB_DATA = {
|
||||
"TEST_USER_ID": "@bob:xyz",
|
||||
"TEST_DEVICE_ID": "bob_device",
|
||||
"TEST_ROOM_ID": "!room:id",
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef",
|
||||
|
||||
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
|
||||
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
|
||||
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
|
||||
|
||||
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
|
||||
"B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
|
||||
|
||||
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
|
||||
}
|
||||
|
||||
def main() -> None:
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
TEST_DEVICE_PRIVATE_KEY_BYTES
|
||||
print(
|
||||
f"""\
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
// Alice data
|
||||
|
||||
{build_test_data(ALICE_DATA)}
|
||||
// Bob data
|
||||
|
||||
{build_test_data(BOB_DATA, "BOB_")}
|
||||
""",
|
||||
end="",
|
||||
)
|
||||
|
||||
# Use static seed to have stable random test data upon new generation
|
||||
seed(10)
|
||||
|
||||
def build_test_data(user_data, prefix = "") -> str:
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_public_key = encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
device_data = {
|
||||
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
"device_id": TEST_DEVICE_ID,
|
||||
"device_id": user_data["TEST_DEVICE_ID"],
|
||||
"keys": {
|
||||
f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
f"ed25519:{TEST_DEVICE_ID}": b64_public_key,
|
||||
f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key,
|
||||
},
|
||||
"signatures": {TEST_USER_ID: {}},
|
||||
"user_id": TEST_USER_ID,
|
||||
"signatures": {user_data['TEST_USER_ID']: {}},
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
}
|
||||
|
||||
device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
|
||||
device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json(
|
||||
device_data, private_key
|
||||
)
|
||||
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
b64_master_private_key = encode_base64(MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_self_signing_private_key = encode_base64(SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
b64_user_signing_private_key = encode_base64(USER_CROSS_SIGNING_PRIVATE_KEY_BYTES)
|
||||
b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
|
||||
|
||||
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
|
||||
base64.b64decode(B64_BACKUP_DECRYPTION_KEY)
|
||||
base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
)
|
||||
b64_backup_public_key = encode_base64(
|
||||
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
@@ -113,75 +161,96 @@ def main() -> None:
|
||||
# sign with our device key
|
||||
sig = sign_json(backup_data["auth_data"], private_key)
|
||||
backup_data["auth_data"]["signatures"] = {
|
||||
TEST_USER_ID: {f"ed25519:{TEST_DEVICE_ID}": sig}
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig}
|
||||
}
|
||||
|
||||
print(
|
||||
f"""\
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
set_of_exported_room_keys = [build_exported_megolm_key(), build_exported_megolm_key()]
|
||||
|
||||
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
import {{ KeyBackupInfo }} from "../../../src/crypto-api";
|
||||
additional_exported_room_key = build_exported_megolm_key()
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
otk_to_sign = {
|
||||
"key": user_data['OTK']
|
||||
}
|
||||
# sign our public otk key with our device key
|
||||
otk = sign_json(otk_to_sign, private_key)
|
||||
otks = {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
user_data['TEST_DEVICE_ID']: {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": user_data["OTK"],
|
||||
"signatures": {
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TEST_USER_ID = "{TEST_USER_ID}";
|
||||
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
|
||||
export const TEST_ROOM_ID = "{TEST_ROOM_ID}";
|
||||
return f"""\
|
||||
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
|
||||
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
|
||||
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
||||
export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
||||
export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
|
||||
export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
|
||||
|
||||
/** base64-encoded private master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
|
||||
export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
|
||||
|
||||
/** base64-encoded public self cross-signing key */
|
||||
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
|
||||
export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
|
||||
|
||||
/** base64-encoded private self signing cross-signing key */
|
||||
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
|
||||
export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
|
||||
|
||||
/** base64-encoded public user cross-signing key */
|
||||
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
|
||||
export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
|
||||
|
||||
/** base64-encoded private user signing cross-signing key */
|
||||
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
|
||||
export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(), indent=4)
|
||||
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "{ B64_BACKUP_DECRYPTION_KEY }";
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
""",
|
||||
end="",
|
||||
)
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
};
|
||||
|
||||
/** An exported megolm session */
|
||||
export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
json.dumps(additional_exported_room_key, indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
"""
|
||||
|
||||
|
||||
def build_cross_signing_keys_data() -> dict:
|
||||
def build_cross_signing_keys_data(user_data) -> dict:
|
||||
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
@@ -189,7 +258,7 @@ def build_cross_signing_keys_data() -> dict:
|
||||
)
|
||||
)
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
@@ -199,39 +268,39 @@ def build_cross_signing_keys_data() -> dict:
|
||||
# create without signatures initially
|
||||
cross_signing_keys_data = {
|
||||
"master_keys": {
|
||||
TEST_USER_ID: {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_master_public_key}": b64_master_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["master"],
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
TEST_USER_ID: {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["self_signing"],
|
||||
},
|
||||
},
|
||||
"user_signing_keys": {
|
||||
TEST_USER_ID: {
|
||||
user_data["TEST_USER_ID"]: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"user_id": user_data["TEST_USER_ID"],
|
||||
"usage": ["user_signing"],
|
||||
},
|
||||
},
|
||||
}
|
||||
# sign the sub-keys with the master
|
||||
for k in ["self_signing_keys", "user_signing_keys"]:
|
||||
to_sign = cross_signing_keys_data[k][TEST_USER_ID]
|
||||
to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]]
|
||||
sig = sign_json(to_sign, master_private_key)
|
||||
to_sign["signatures"] = {
|
||||
TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig}
|
||||
user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig}
|
||||
}
|
||||
|
||||
return cross_signing_keys_data
|
||||
@@ -265,6 +334,39 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
|
||||
return signature_base64
|
||||
|
||||
def build_exported_megolm_key() -> dict:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
"""
|
||||
index = 0
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32))
|
||||
# Just use radom bytes for the ratchet parts
|
||||
ratchet = randbytes(32 * 4)
|
||||
# exported key, start with version byte
|
||||
exported_key = bytearray(b'\x01')
|
||||
exported_key += index.to_bytes(4, 'big')
|
||||
exported_key += ratchet
|
||||
# KPub
|
||||
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
|
||||
|
||||
megolm_export = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
|
||||
return megolm_export
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult } from "../../../src";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
// Alice data
|
||||
|
||||
export const TEST_USER_ID = "@alice:localhost";
|
||||
export const TEST_DEVICE_ID = "test_device";
|
||||
export const TEST_ROOM_ID = "!room:id";
|
||||
@@ -116,3 +118,224 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "FYOoKQSwe4d9jhTZ/LQCZFJINjPEqZ7Or4Z08reP92M",
|
||||
"session_key": "AQAAAABZ0jXQOprFfXe41tIFmAtHxflJp4O2hM/vzQQpOazOCFeWSoW5P3Z9Q+voU3eXehMwyP8/hm/Q8xLP6/PmJdy+71se/17kdFwcDGgLxBWfa4ODM9zlI4EjKbNqmiii5loJ7rBhA/XXaw80m0hfU6zTDX/KrO55J0Pt4vJ0LDa3LBWDqCkEsHuHfY4U2fy0AmRSSDYzxKmezq+GdPK3j/dj",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "mPYSGA2l1tOQiipEDEVYhDSdTSFh2lDW1qpGKYZRxTc",
|
||||
"session_key": "AQAAAAAHwgkB49BTPAEGTCK6degxUIbl8GPG2ugPRYhNtOpNic63u11+baXFfjDw5fmVfD1gJXpQQjGsqrIYioxrB1xzl7mfb942UHhYdaMQZowpp1fSpJVsxR5TddUU2EWifYD9EQsoz8mY1zqoazm4vUP4v9yxaTcUBj2c6HMJCY0gCJj2EhgNpdbTkIoqRAxFWIQ0nU0hYdpQ1taqRimGUcU3",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
];
|
||||
|
||||
/** An exported megolm session */
|
||||
export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
|
||||
"session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const ONE_TIME_KEYS = {
|
||||
"@alice:localhost": {
|
||||
"test_device": {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
export const BOB_TEST_DEVICE_ID = "bob_device";
|
||||
export const BOB_TEST_ROOM_ID = "!room:id";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const BOB_SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "bob_device",
|
||||
"keys": {
|
||||
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
"ed25519:bob_device": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA";
|
||||
|
||||
/** base64-encoded private master cross-signing key */
|
||||
export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
|
||||
|
||||
/** base64-encoded public self cross-signing key */
|
||||
export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A";
|
||||
|
||||
/** base64-encoded private self signing cross-signing key */
|
||||
export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
|
||||
|
||||
/** base64-encoded public user cross-signing key */
|
||||
export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw";
|
||||
|
||||
/** base64-encoded private user signing cross-signing key */
|
||||
export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "X9SK5JceUdvUr9gZkmdfS78qmKztoL60oORJ9JA5XD8",
|
||||
"session_key": "AQAAAADbfOdUj/ec5bK4xVEhhRw+jfd1FD4uA0MLy7NyVHugZxyXNZUx5YEof4H9YyjBretviZreMSXqflbgYKz257rkKu7MPeKFf7zmln2GxX0F/p++GOnvpY1FqOglhfRQi3tqiyOa7SL4f7TuERDTOpMqlWhIfTKQnqy0AyF2vpDi5V/UiuSXHlHb1K/YGZJnX0u/Kpis7aC+tKDkSfSQOVw/",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "ZG6lrfATe+958wN1xaGf3dKG/CThEfkmNdp1jcu4zok"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA",
|
||||
"session_key": "AQAAAACv0khqPrQ91MmWCgm0RTzfpn65AGCrRnAKLxGJdfSfECNZ8gyj34FZLwi+F+xC6ibFddcbLXW0mzR6PnTnHF3VHM4g/h+2rcxtlix8fySpIwFzaXViba7cOSy/b+dHTMZB40iA7F4y7AdTdHLv4N1XUj3puU/KVUIKf9/lEDLqyReD+39WdEY24mTIB5NcQQhtyguPzYPT5sSyeIUNd7Bw",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "HxUKnGfeUu0fF3cLyCFSDXYtVCQHy/+33q9RkzKfsiU"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
}
|
||||
];
|
||||
|
||||
/** An exported megolm session */
|
||||
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!roomA:example.org",
|
||||
"sender_key": "/Bu9e34hUClhddpf4E5gu5qEAdMY31+1A9HbiAeeQgo",
|
||||
"session_id": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM",
|
||||
"session_key": "AQAAAACvcoGk7mOY59fOqZaxFUiTCBRV1Ia94KBjAZx6kgdgBtkkvs50z8od8/Nc9ncK2UsEiXNvCTTp2dlN3du+Rx0/m7vet2ZOEEp2oYDjHMLLFmwd1gtlGuWYPdXA6Y1+9Yyph0/EDVfS+zd3XvbL0QgbyL43+yQnFNHKlxVJX1eiKTrGTHQtYEOZz6/i/bbk+sV7GhSZFT5IMT9hXsRxdf0D",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "dV0TIhhkToXpL+gZLo+zXDHJfw7MWYxpg80cynIQDv0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const BOB_ONE_TIME_KEYS = {
|
||||
"@bob:xyz": {
|
||||
"bob_device": {
|
||||
"signed_curve25519:AAAAHQ": {
|
||||
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { M_AUTHENTICATION } from "../../src";
|
||||
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcError } from "../../src/oidc/error";
|
||||
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
|
||||
@@ -351,7 +351,7 @@ describe("AutoDiscovery", function () {
|
||||
function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
not_matrix_versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -388,7 +388,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -428,7 +428,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -469,7 +469,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -515,7 +515,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -560,7 +560,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
@@ -606,7 +606,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -653,7 +653,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -697,7 +697,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
@@ -747,7 +747,7 @@ describe("AutoDiscovery", function () {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
@@ -867,6 +867,37 @@ describe("AutoDiscovery", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should FAIL_ERROR for unsupported Matrix version", () => {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.6.0"],
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("m.authentication", () => {
|
||||
const homeserverName = "example.org";
|
||||
const homeserverUrl = "https://chat.example.org/";
|
||||
@@ -879,7 +910,7 @@ describe("AutoDiscovery", function () {
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["r0.0.1"] });
|
||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.1"] });
|
||||
|
||||
fetchMock.get("https://example.org/.well-known/matrix/client", {
|
||||
"m.homeserver": {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("ContentRepo", function () {
|
||||
it("should return a download URL if no width/height/resize are specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,21 +44,21 @@ describe("ContentRepo", function () {
|
||||
it("should return a thumbnail URL if a width/height/resize is specified", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,6 +312,7 @@ describe("Secrets", function () {
|
||||
this.emit(ClientEvent.AccountData, event);
|
||||
return {};
|
||||
};
|
||||
bob.getKeyBackupVersion = jest.fn().mockResolvedValue(null);
|
||||
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
|
||||
@@ -6,6 +6,6 @@ exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`]
|
||||
"params": {
|
||||
"access_token": "token",
|
||||
},
|
||||
"path": "/_matrix/media/r0/upload",
|
||||
"path": "/_matrix/media/v3/upload",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("MatrixHttpApi", () => {
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token",
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
||||
);
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
@@ -96,7 +96,7 @@ describe("MatrixHttpApi", () => {
|
||||
accessToken: "token",
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
@@ -105,14 +105,14 @@ describe("MatrixHttpApi", () => {
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name",
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
|
||||
@@ -201,7 +201,7 @@ describe("MatrixClient", function () {
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve({
|
||||
unstable_features: unstableFeatures,
|
||||
versions: ["r0.6.0", "r0.6.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
}
|
||||
const next = httpLookups.shift();
|
||||
@@ -2266,7 +2266,6 @@ describe("MatrixClient", function () {
|
||||
function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
"creator": "@daryl:alexandria.example.com",
|
||||
"m.federate": true,
|
||||
"predecessor": {
|
||||
event_id: "id_of_last_event",
|
||||
|
||||
@@ -195,7 +195,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
|
||||
it("retries on retryImmediately()", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
@@ -219,7 +219,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
|
||||
it("retries on when client is started", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
|
||||
@@ -243,7 +243,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
|
||||
it("retries when a message is retried", async function () {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
|
||||
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
|
||||
|
||||
@@ -70,9 +70,7 @@ describe("RoomState", function () {
|
||||
user: userA,
|
||||
room: roomId,
|
||||
event: true,
|
||||
content: {
|
||||
creator: userA,
|
||||
},
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
+35
-4
@@ -228,7 +228,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
describe("getCreator", () => {
|
||||
it("should return the creator from m.room.create", function () {
|
||||
it("should return the sender from m.room.create", function () {
|
||||
// @ts-ignore - mocked doesn't handle overloads sanely
|
||||
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
||||
if (type === EventType.RoomCreate && key === "") {
|
||||
@@ -239,7 +239,7 @@ describe("Room", function () {
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
creator: userA,
|
||||
creator: userB, // The creator field was dropped in room version 11 but a malicious client might still send it
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -247,6 +247,24 @@ describe("Room", function () {
|
||||
const roomCreator = room.getCreator();
|
||||
expect(roomCreator).toStrictEqual(userA);
|
||||
});
|
||||
|
||||
it("should return null if the sender is undefined", function () {
|
||||
// @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,
|
||||
type: EventType.RoomCreate,
|
||||
skey: "",
|
||||
room: roomId,
|
||||
user: undefined,
|
||||
content: {},
|
||||
});
|
||||
}
|
||||
});
|
||||
const roomCreator = room.getCreator();
|
||||
expect(roomCreator).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function () {
|
||||
@@ -1184,6 +1202,21 @@ describe("Room", function () {
|
||||
room.recalculate();
|
||||
expect(room.name).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("emits an update event", function () {
|
||||
const spy = jest.fn();
|
||||
const summary = {
|
||||
"m.heroes": [],
|
||||
"m.invited_member_count": 1,
|
||||
};
|
||||
|
||||
room.once(RoomEvent.Summary, spy);
|
||||
|
||||
room.setSummary(summary);
|
||||
room.recalculate();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(summary);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room.recalculate => Room Name", function () {
|
||||
@@ -3471,12 +3504,10 @@ describe("Room", function () {
|
||||
|
||||
function roomCreateEvent(newRoomId: string, predecessorRoomId: string | null): MatrixEvent {
|
||||
const content: {
|
||||
creator: string;
|
||||
["m.federate"]: boolean;
|
||||
room_version: string;
|
||||
predecessor: { event_id: string; room_id: string } | undefined;
|
||||
} = {
|
||||
"creator": "@daryl:alexandria.example.com",
|
||||
"predecessor": undefined,
|
||||
"m.federate": true,
|
||||
"room_version": "9",
|
||||
|
||||
@@ -29,8 +29,9 @@ import {
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi, UIAuthCallback } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
describe("OutgoingRequestProcessor", () => {
|
||||
/** the OutgoingRequestProcessor implementation under test */
|
||||
@@ -161,7 +162,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
.when("PUT", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(
|
||||
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
|
||||
"https://example.com/_matrix/client/v3/rooms/test%2Froom/send/test%2Ftype/test%2Ftxnid",
|
||||
);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
@@ -218,4 +219,40 @@ describe("OutgoingRequestProcessor", () => {
|
||||
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
});
|
||||
|
||||
it("does not explode if the OlmMachine is stopped while the request is in flight", async () => {
|
||||
// we use a real olm machine for this test
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
|
||||
new RustSdkCryptoJs.UserId("@alice:example.com"),
|
||||
new RustSdkCryptoJs.DeviceId("TEST_DEVICE"),
|
||||
);
|
||||
|
||||
const authRequestResultDefer = defer<string>();
|
||||
|
||||
const authRequestCalledPromise = new Promise<void>((resolve) => {
|
||||
const mockHttpApi = {
|
||||
authedRequest: async () => {
|
||||
resolve();
|
||||
return await authRequestResultDefer.promise;
|
||||
},
|
||||
} as unknown as Mocked<MatrixHttpApi<IHttpOpts & { onlyData: true }>>;
|
||||
processor = new OutgoingRequestProcessor(olmMachine, mockHttpApi);
|
||||
});
|
||||
|
||||
// build a request
|
||||
const request = olmMachine.queryKeysForUsers([new RustSdkCryptoJs.UserId("@bob:example.com")]);
|
||||
const result = processor.makeOutgoingRequest(request);
|
||||
|
||||
// wait for the HTTP request to be made
|
||||
await authRequestCalledPromise;
|
||||
|
||||
// while the HTTP request is in flight, the OlmMachine gets stopped.
|
||||
olmMachine.close();
|
||||
|
||||
// the HTTP request completes...
|
||||
authRequestResultDefer.resolve("{}");
|
||||
|
||||
// ... and `makeOutgoingRequest` resolves satisfactorily
|
||||
await result;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,32 +61,7 @@ describe("RustCrypto", () => {
|
||||
);
|
||||
|
||||
it("should import and export keys", async () => {
|
||||
const someRoomKeys = [
|
||||
{
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: "!cLDYAnjpiQXIrSwngM:localhost:8480",
|
||||
sender_key: "C9FMqTD20C0VaGWE/aSImkimuE6HDa/RyYj5gRUg3gY",
|
||||
session_id: "iGQG5GaP1/B3dSH6zCQDQqrNuotrtQjVC7w1OsUDwbg",
|
||||
session_key:
|
||||
"AQAAAADaCbP2gdOy8jrhikjploKgSBaFSJ5rvHcziaADbwNEzeCSrfuAUlXvCvxik8kU+MfCHIi5arN2M7UM5rGKdzkHnkReoIByFkeMdbjKWk5SFpVQexcM74eDhBGj+ICkQqOgApfnEbSswrmreB0+MhHHyLStwW5fy5f8A9QW1sbPuohkBuRmj9fwd3Uh+swkA0KqzbqLa7UI1Qu8NTrFA8G4",
|
||||
sender_claimed_keys: {
|
||||
ed25519: "RSq0Xw0RR0DeqlJ/j3qrF5qbN0D96fKk8lz9kZJlG9k",
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
},
|
||||
{
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: "!cLDYAnjpiQXIrSwngM:localhost:8480",
|
||||
sender_key: "C9FMqTD20C0VaGWE/aSImkimuE6HDa/RyYj5gRUg3gY",
|
||||
session_id: "P/Jy9Tog4CMtLseeS4Fe2AEXZov3k6cibcop/uyhr78",
|
||||
session_key:
|
||||
"AQAAAAATyAVm0c9c9DW9Od72MxvfSDYoysBw3C6yMJ3bYuTmssHN7yNGm59KCtKeFp2Y5qO7lvUmwOfSTvTASUb7HViE7Lt+Bvp5WiMTJ2Pv6m+N12ihyowV5lgtKFWI18Wxd0AugMTVQRwjBK6aMobf86NXWD2hiKm3N6kWbC0PXmqV7T/ycvU6IOAjLS7HnkuBXtgBF2aL95OnIm3KKf7soa+/",
|
||||
sender_claimed_keys: {
|
||||
ed25519: "RSq0Xw0RR0DeqlJ/j3qrF5qbN0D96fKk8lz9kZJlG9k",
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
},
|
||||
];
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
let importTotal = 0;
|
||||
const opt: ImportRoomKeysOpts = {
|
||||
progressCallback: (stage) => {
|
||||
@@ -95,11 +70,11 @@ describe("RustCrypto", () => {
|
||||
};
|
||||
await rustCrypto.importRoomKeys(someRoomKeys, opt);
|
||||
|
||||
expect(importTotal).toBe(2);
|
||||
expect(importTotal).toBe(someRoomKeys.length);
|
||||
|
||||
const keys = await rustCrypto.exportRoomKeys();
|
||||
expect(Array.isArray(keys)).toBeTruthy();
|
||||
expect(keys.length).toBe(2);
|
||||
expect(keys.length).toBe(someRoomKeys.length);
|
||||
|
||||
const aSession = someRoomKeys[0];
|
||||
|
||||
@@ -175,6 +150,44 @@ describe("RustCrypto", () => {
|
||||
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
|
||||
});
|
||||
|
||||
describe("getCrossSigningStatus", () => {
|
||||
it("returns sensible values on a default client", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||
|
||||
const result = await rustCrypto.getCrossSigningStatus();
|
||||
|
||||
expect(secretStorage.isStored).toHaveBeenCalledWith("m.cross_signing.master");
|
||||
expect(result).toEqual({
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: false,
|
||||
selfSigningKey: false,
|
||||
userSigningKey: false,
|
||||
},
|
||||
privateKeysInSecretStorage: false,
|
||||
publicKeysOnDevice: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws if `stop` is called mid-call", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||
|
||||
// start the call off
|
||||
const result = rustCrypto.getCrossSigningStatus();
|
||||
|
||||
// call `.stop`
|
||||
rustCrypto.stop();
|
||||
|
||||
// getCrossSigningStatus should abort
|
||||
await expect(result).rejects.toEqual(new Error("MatrixClient has been stopped"));
|
||||
});
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning delegates to CrossSigningIdentity", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const mockCrossSigningIdentity = {
|
||||
@@ -485,6 +498,23 @@ describe("RustCrypto", () => {
|
||||
|
||||
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true if the user is untracked, downloadUncached is set at true and the cross-signing keys are available", async () => {
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
device_keys: {
|
||||
[testData.BOB_TEST_USER_ID]: {
|
||||
[testData.BOB_TEST_DEVICE_ID]: testData.BOB_SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
...testData.BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
});
|
||||
|
||||
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID, true)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if the user is unknown", async () => {
|
||||
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRecoveryKeyFromPassphrase", () => {
|
||||
@@ -585,6 +615,52 @@ describe("RustCrypto", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestVerificationDM", () => {
|
||||
it("send verification request to an unknown user", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(() =>
|
||||
rustCrypto.requestVerificationDM("@bob:example.com", testData.TEST_ROOM_ID),
|
||||
).rejects.toThrow("unknown userId @bob:example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserVerificationStatus", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(() => {
|
||||
olmMachine = {
|
||||
getIdentity: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(
|
||||
olmMachine,
|
||||
{} as MatrixClient["http"],
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
{} as CryptoCallbacks,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
|
||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||
expect(userVerificationStatus.isVerified()).toBeFalsy();
|
||||
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
|
||||
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
|
||||
olmMachine.getIdentity.mockResolvedValue({ isVerified: jest.fn().mockReturnValue(true) });
|
||||
|
||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||
expect(userVerificationStatus.isVerified()).toBeTruthy();
|
||||
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a basic RustCrypto instance for testing
|
||||
|
||||
@@ -17,8 +17,9 @@ limitations under the License.
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import { RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||
import { isVerificationEvent, RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { EventType, MatrixEvent, MsgType } from "../../../src";
|
||||
|
||||
describe("VerificationRequest", () => {
|
||||
describe("pending", () => {
|
||||
@@ -80,6 +81,40 @@ describe("VerificationRequest", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVerificationEvent", () => {
|
||||
it.each([
|
||||
[EventType.KeyVerificationCancel],
|
||||
[EventType.KeyVerificationDone],
|
||||
[EventType.KeyVerificationMac],
|
||||
[EventType.KeyVerificationStart],
|
||||
[EventType.KeyVerificationKey],
|
||||
[EventType.KeyVerificationReady],
|
||||
[EventType.KeyVerificationAccept],
|
||||
])("should return true with %s event", (eventType) => {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
});
|
||||
expect(isVerificationEvent(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true with EventType.RoomMessage and MsgType.KeyVerificationRequest", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: MsgType.KeyVerificationRequest,
|
||||
},
|
||||
});
|
||||
expect(isVerificationEvent(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false with a non verification event", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
});
|
||||
expect(isVerificationEvent(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/** build a RustVerificationRequest with default parameters */
|
||||
function makeTestRequest(
|
||||
inner?: RustSdkCryptoJs.VerificationRequest,
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
|
||||
const FAKE_ROOM_ID = "!foo:bar";
|
||||
const CALL_LIFETIME = 60000;
|
||||
@@ -1812,4 +1813,30 @@ describe("Call", function () {
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit IceFailed error on the successor call if RTCPeerConnection throws", async () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
});
|
||||
|
||||
const call = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: "!room_id",
|
||||
});
|
||||
const successor = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: "!room_id",
|
||||
});
|
||||
call.replacedBy(successor);
|
||||
|
||||
const prom = emitPromise(successor, CallEvent.Error);
|
||||
call.placeCall(true, true);
|
||||
|
||||
const err = await prom;
|
||||
expect(err.code).toBe(CallErrorCode.IceFailed);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-28
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IClearEvent } from "../models/event";
|
||||
import type { ISignatures } from "./signed";
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
@@ -22,33 +21,8 @@ export type OlmGroupSessionExtraData = {
|
||||
sharedHistory?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to {@link Crypto.decryptEvent}
|
||||
*/
|
||||
export interface IEventDecryptionResult {
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*/
|
||||
clearEvent: IClearEvent;
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
* See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
*/
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
/**
|
||||
* Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
|
||||
*/
|
||||
senderCurve25519Key?: string;
|
||||
/**
|
||||
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
/**
|
||||
* The sender doesn't authorize the unverified devices to decrypt his messages
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
// Backwards compatible re-export
|
||||
export type { EventDecryptionResult as IEventDecryptionResult } from "../common-crypto/CryptoBackend";
|
||||
|
||||
interface Extensible {
|
||||
[key: string]: any;
|
||||
|
||||
@@ -193,12 +193,6 @@ export interface IBindThreePidBody {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IAddThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
|
||||
+19
-1
@@ -28,6 +28,7 @@ import {
|
||||
validateWellKnownAuthentication,
|
||||
} from "./oidc/validate";
|
||||
import { OidcError } from "./oidc/error";
|
||||
import { MINIMUM_MATRIX_VERSION } from "./version-support";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -50,6 +51,9 @@ enum AutoDiscoveryError {
|
||||
InvalidIs = "Invalid identity server discovery response",
|
||||
MissingWellknown = "No .well-known JSON file found",
|
||||
InvalidJson = "Invalid JSON",
|
||||
HomeserverTooOld = "The homeserver does not meet the minimum version requirements",
|
||||
// TODO: Implement when Sydent supports the `/versions` endpoint - https://github.com/matrix-org/sydent/issues/424
|
||||
//IdentityServerTooOld = "The identity server does not meet the minimum version requirements",
|
||||
}
|
||||
|
||||
interface AutoDiscoveryState {
|
||||
@@ -108,6 +112,8 @@ export class AutoDiscovery {
|
||||
|
||||
public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
|
||||
|
||||
public static readonly ERROR_HOMESERVER_TOO_OLD = AutoDiscoveryError.HomeserverTooOld;
|
||||
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
|
||||
|
||||
/**
|
||||
@@ -199,7 +205,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this.fetchWellKnownObject<IServerVersions>(`${hsUrl}/_matrix/client/versions`);
|
||||
if (!hsVersions?.raw?.["versions"]) {
|
||||
if (!hsVersions || !Array.isArray(hsVersions.raw?.["versions"])) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
|
||||
@@ -210,6 +216,18 @@ export class AutoDiscovery {
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 3.1: Non-spec check to ensure the server will actually work for us
|
||||
if (!hsVersions.raw!["versions"].includes(MINIMUM_MATRIX_VERSION)) {
|
||||
logger.error("Homeserver does not meet minimum version requirements");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_HOMESERVER_TOO_OLD;
|
||||
|
||||
// Supply the base_url to the caller because they may be ignoring liveliness
|
||||
// errors, like this one.
|
||||
clientConfig["m.homeserver"].base_url = hsUrl;
|
||||
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 4: Now that the homeserver looks valid, update our client config.
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
|
||||
+43
-201
@@ -133,7 +133,6 @@ import {
|
||||
IFilterResponse,
|
||||
ITagsResponse,
|
||||
IStatusResponse,
|
||||
IAddThreePidBody,
|
||||
KnockRoomOpts,
|
||||
} from "./@types/requests";
|
||||
import {
|
||||
@@ -149,7 +148,7 @@ import {
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
MSC3912_RELATION_BASED_REDACTIONS_PROP,
|
||||
} from "./@types/event";
|
||||
import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials";
|
||||
import { IdServerUnbindResult, IImageInfo, JoinRule, Preset, Visibility } from "./@types/partials";
|
||||
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
|
||||
import { randomString } from "./randomstring";
|
||||
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup";
|
||||
@@ -737,6 +736,7 @@ export interface IPublicRoomsChunkRoom {
|
||||
guest_can_join: boolean;
|
||||
num_joined_members: number;
|
||||
room_type?: RoomType | string; // Added by MSC3827
|
||||
join_rule?: JoinRule.Knock | JoinRule.Public; // Added by MSC2403
|
||||
}
|
||||
|
||||
interface IPublicRoomsResponse {
|
||||
@@ -1287,7 +1287,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
baseUrl: opts.baseUrl,
|
||||
idBaseUrl: opts.idBaseUrl,
|
||||
accessToken: opts.accessToken,
|
||||
prefix: ClientPrefix.R0,
|
||||
prefix: ClientPrefix.V3,
|
||||
onlyData: true,
|
||||
extraParams: opts.queryParams,
|
||||
localTimeoutMs: opts.localTimeoutMs,
|
||||
@@ -2252,6 +2252,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.reEmitter.reEmit(rustCrypto, [
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
CryptoEvent.KeyBackupStatus,
|
||||
CryptoEvent.KeyBackupSessionsRemaining,
|
||||
CryptoEvent.KeyBackupFailed,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2442,6 +2445,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @returns resolves to a VerificationRequest
|
||||
* when the request has been sent to the other party.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.requestVerificationDM}.
|
||||
*/
|
||||
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||
if (!this.crypto) {
|
||||
@@ -2623,6 +2628,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
* @deprecated Prefer {@link CryptoApi#userHasCrossSigningKeys}
|
||||
*/
|
||||
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2637,6 +2643,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - The ID of the user to check.
|
||||
*
|
||||
* @deprecated Use {@link Crypto.CryptoApi.getUserVerificationStatus | `CryptoApi.getUserVerificationStatus`}
|
||||
*/
|
||||
public checkUserTrust(userId: string): UserTrustLevel {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -3251,6 +3259,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
|
||||
if (!this.crypto) {
|
||||
@@ -3262,6 +3272,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Get information about the current key backup.
|
||||
* @returns Information object from API or null
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
|
||||
let res: IKeyBackupInfo;
|
||||
@@ -3320,6 +3332,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param info - Backup information object as returned by getKeyBackupVersion
|
||||
* @returns Promise which resolves when complete.
|
||||
*
|
||||
* @deprecated Do not call this directly. Instead call {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
@@ -3331,6 +3345,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*
|
||||
* @deprecated It should be unnecessary to disable key backup.
|
||||
*/
|
||||
public disableKeyBackup(): void {
|
||||
if (!this.crypto) {
|
||||
@@ -3350,6 +3366,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @returns Object that can be passed to createKeyBackupVersion and
|
||||
* additionally has a 'recovery_key' member with the user-facing recovery key string.
|
||||
*
|
||||
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
|
||||
*/
|
||||
public async prepareKeyBackupVersion(
|
||||
password?: string | Uint8Array | null,
|
||||
@@ -3393,6 +3411,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param info - Info object from prepareKeyBackupVersion
|
||||
* @returns Object with 'version' param indicating the version created
|
||||
*
|
||||
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
|
||||
*/
|
||||
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
|
||||
if (!this.crypto) {
|
||||
@@ -3425,9 +3445,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
await this.crypto.crossSigningInfo.signObject(data.auth_data, "master");
|
||||
}
|
||||
|
||||
const res = await this.http.authedRequest<IKeyBackupInfo>(Method.Post, "/room_keys/version", undefined, data, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
const res = await this.http.authedRequest<IKeyBackupInfo>(Method.Post, "/room_keys/version", undefined, data);
|
||||
|
||||
// We could assume everything's okay and enable directly, but this ensures
|
||||
// we run the same signature verification that will be used for future
|
||||
@@ -3440,24 +3458,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Crypto.CryptoApi.deleteKeyBackupVersion | `CryptoApi.deleteKeyBackupVersion`}.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeyBackupVersion
|
||||
// so this is symmetrical).
|
||||
// TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version.
|
||||
if (this.crypto.backupManager.version) {
|
||||
this.crypto.backupManager.disableKeyBackup();
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/room_keys/version/$version", {
|
||||
$version: version,
|
||||
});
|
||||
|
||||
await this.http.authedRequest(Method.Delete, path, undefined, undefined, { prefix: ClientPrefix.V3 });
|
||||
await this.cryptoBackend.deleteKeyBackupVersion(version);
|
||||
}
|
||||
|
||||
private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath;
|
||||
@@ -3591,7 +3600,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Restores all rooms if omitted.
|
||||
* @param targetSessionId - Session ID to target a specific session.
|
||||
* Restores all sessions if omitted.
|
||||
* @param backupInfo - Backup metadata from `checkKeyBackup`
|
||||
* @param backupInfo - Backup metadata from `getKeyBackupVersion` or `checkKeyBackup`.`backupInfo`
|
||||
* @param opts - Optional params such as callbacks
|
||||
* @returns Status of restoration with `total` and `imported`
|
||||
* key counts.
|
||||
@@ -3924,7 +3933,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public getMediaConfig(): Promise<IMediaConfig> {
|
||||
return this.http.authedRequest(Method.Get, "/config", undefined, undefined, {
|
||||
prefix: MediaPrefix.R0,
|
||||
prefix: MediaPrefix.V3,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5179,7 +5188,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
prefix: MediaPrefix.R0,
|
||||
prefix: MediaPrefix.V3,
|
||||
priority: "low",
|
||||
},
|
||||
);
|
||||
@@ -5346,7 +5355,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
address: address,
|
||||
};
|
||||
|
||||
if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
|
||||
if (this.identityServer?.getAccessToken) {
|
||||
const identityAccessToken = await this.identityServer.getAccessToken();
|
||||
if (identityAccessToken) {
|
||||
params["id_access_token"] = identityAccessToken;
|
||||
@@ -6138,7 +6147,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const opts = {
|
||||
prefix:
|
||||
Thread.hasServerSideListSupport === FeatureSupport.Stable
|
||||
? "/_matrix/client/v1"
|
||||
? ClientPrefix.V1
|
||||
: "/_matrix/client/unstable/org.matrix.msc3856",
|
||||
};
|
||||
|
||||
@@ -6664,20 +6673,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
): Promise<T> {
|
||||
const postParams = Object.assign({}, params);
|
||||
|
||||
// If the HS supports separate add and bind, then requestToken endpoints
|
||||
// don't need an IS as they are all validated by the HS directly.
|
||||
if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) {
|
||||
const idServerUrl = new URL(this.idBaseUrl);
|
||||
postParams.id_server = idServerUrl.host;
|
||||
|
||||
if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
|
||||
const identityAccessToken = await this.identityServer.getAccessToken();
|
||||
if (identityAccessToken) {
|
||||
postParams.id_access_token = identityAccessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.request(Method.Post, endpoint, undefined, postParams);
|
||||
}
|
||||
|
||||
@@ -7382,78 +7377,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return versions && versions.includes(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if it supports members lazy loading
|
||||
* @returns true if server supports lazy loading
|
||||
*/
|
||||
public async doesServerSupportLazyLoading(): Promise<boolean> {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
|
||||
return (
|
||||
(versions && versions.includes("r0.5.0")) || (unstableFeatures && unstableFeatures["m.lazy_load_members"])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if the `id_server` parameter is required
|
||||
* when registering with an 3pid, adding a 3pid or resetting password.
|
||||
* @returns true if id_server parameter is required
|
||||
*/
|
||||
public async doesServerRequireIdServerParam(): Promise<boolean> {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return true;
|
||||
|
||||
const versions = response["versions"];
|
||||
|
||||
// Supporting r0.6.0 is the same as having the flag set to false
|
||||
if (versions && versions.includes("r0.6.0")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
if (!unstableFeatures) return true;
|
||||
if (unstableFeatures["m.require_identity_server"] === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
return unstableFeatures["m.require_identity_server"];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if the `id_access_token` parameter can be safely
|
||||
* passed to the homeserver. Some homeservers may trigger errors if they are not
|
||||
* prepared for the new parameter.
|
||||
* @returns true if id_access_token can be sent
|
||||
*/
|
||||
public async doesServerAcceptIdentityAccessToken(): Promise<boolean> {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
return (versions && versions.includes("r0.6.0")) || (unstableFeatures && unstableFeatures["m.id_access_token"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if it supports separate 3PID add and bind functions.
|
||||
* This affects the sequence of API calls clients should use for these operations,
|
||||
* so it's helpful to be able to check for support.
|
||||
* @returns true if separate functions are supported
|
||||
*/
|
||||
public async doesServerSupportSeparateAddAndBind(): Promise<boolean> {
|
||||
const response = await this.getVersions();
|
||||
if (!response) return false;
|
||||
|
||||
const versions = response["versions"];
|
||||
const unstableFeatures = response["unstable_features"];
|
||||
|
||||
return versions?.includes("r0.6.0") || unstableFeatures?.["m.separate_add_and_bind"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if it lists support for an unstable feature
|
||||
* in the /versions response
|
||||
@@ -7525,14 +7448,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the server to see if it supports the MSC2457 `logout_devices` parameter when setting password
|
||||
* @returns true if server supports the `logout_devices` parameter
|
||||
*/
|
||||
public doesServerSupportLogoutDevices(): Promise<boolean> {
|
||||
return this.isVersionSupported("r0.6.1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if lazy loading members is being used.
|
||||
* @returns Whether or not members are lazy loaded by this client
|
||||
@@ -7917,18 +7832,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param relayState - URL Callback after SAML2 Authentication
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
* @deprecated this isn't in the Matrix spec anymore
|
||||
*/
|
||||
public loginWithSAML2(relayState: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.saml2", {
|
||||
relay_state: relayState,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param redirectUrl - The URL to redirect to after the HS
|
||||
* authenticates with CAS.
|
||||
@@ -7958,7 +7861,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
[SSO_ACTION_PARAM.unstable!]: action,
|
||||
};
|
||||
|
||||
return this.http.getUrl(url, params, ClientPrefix.R0).href;
|
||||
return this.http.getUrl(url, params).href;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8076,13 +7979,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$loginType: loginType,
|
||||
});
|
||||
|
||||
return this.http.getUrl(
|
||||
path,
|
||||
{
|
||||
session: authSessionId,
|
||||
},
|
||||
ClientPrefix.R0,
|
||||
).href;
|
||||
return this.http.getUrl(path, {
|
||||
session: authSessionId,
|
||||
}).href;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8097,11 +7996,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// inject the id_access_token if inviting 3rd party addresses
|
||||
const invitesNeedingToken = (options.invite_3pid || []).filter((i) => !i.id_access_token);
|
||||
if (
|
||||
invitesNeedingToken.length > 0 &&
|
||||
this.identityServer?.getAccessToken &&
|
||||
(await this.doesServerAcceptIdentityAccessToken())
|
||||
) {
|
||||
if (invitesNeedingToken.length > 0 && this.identityServer?.getAccessToken) {
|
||||
const identityAccessToken = await this.identityServer.getAccessToken();
|
||||
if (identityAccessToken) {
|
||||
for (const invite of invitesNeedingToken) {
|
||||
@@ -8460,30 +8355,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Put, path, undefined, { visibility });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visbility of a room bridged to a 3rd party network in
|
||||
* the current HS's room directory.
|
||||
* @param networkId - the network ID of the 3rd party
|
||||
* instance under which this room is published under.
|
||||
* @param visibility - "public" to make the room visible
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @returns Promise which resolves: result object
|
||||
* @returns Rejects: with an error response.
|
||||
* @deprecated missing from the spec
|
||||
*/
|
||||
public setRoomDirectoryVisibilityAppService(
|
||||
networkId: string,
|
||||
roomId: string,
|
||||
visibility: "public" | "private",
|
||||
): Promise<any> {
|
||||
const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
|
||||
$networkId: networkId,
|
||||
$roomId: roomId,
|
||||
});
|
||||
return this.http.authedRequest(Method.Put, path, undefined, { visibility: visibility });
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the user directory with a term matching user IDs, display names and domains.
|
||||
* @param term - the term with which to search.
|
||||
@@ -8566,31 +8437,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Get, "/account/3pid");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a 3PID to your homeserver account and optionally bind it to an identity
|
||||
* server as well. An identity server is required as part of the `creds` object.
|
||||
*
|
||||
* @deprecated this API is deprecated, and you should instead use `addThreePidOnly` for homeservers that support it.
|
||||
*
|
||||
* @returns Promise which resolves: on success
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public addThreePid(creds: IAddThreePidBody, bind: boolean): Promise<{ submit_url?: string }> {
|
||||
const path = "/account/3pid";
|
||||
const data = {
|
||||
threePidCreds: creds,
|
||||
bind: bind,
|
||||
};
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a 3PID to your homeserver account. This API does not use an identity
|
||||
* server, as the homeserver is expected to handle 3PID ownership validation.
|
||||
*
|
||||
* You can check whether a homeserver supports this API via
|
||||
* `doesServerSupportSeparateAddAndBind`.
|
||||
*
|
||||
* @param data - A object with 3PID validation data from having called
|
||||
* `account/3pid/<medium>/requestToken` on the homeserver.
|
||||
* @returns Promise which resolves: to an empty object `{}`
|
||||
@@ -8598,8 +8448,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
|
||||
const path = "/account/3pid/add";
|
||||
const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8607,9 +8456,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* identity server handles 3PID ownership validation and the homeserver records
|
||||
* the new binding to track where all 3PIDs for the account are bound.
|
||||
*
|
||||
* You can check whether a homeserver supports this API via
|
||||
* `doesServerSupportSeparateAddAndBind`.
|
||||
*
|
||||
* @param data - A object with 3PID validation data from having called
|
||||
* `validate/<medium>/requestToken` on the identity server. It should also
|
||||
* contain `id_server` and `id_access_token` fields as well.
|
||||
@@ -8618,8 +8464,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public async bindThreePid(data: IBindThreePidBody): Promise<{}> {
|
||||
const path = "/account/3pid/bind";
|
||||
const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8644,8 +8489,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
address,
|
||||
id_server: this.getIdentityServerUrl(true),
|
||||
};
|
||||
const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8953,9 +8797,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
public uploadKeySignatures(content: KeySignatures): Promise<IUploadKeySignaturesResponse> {
|
||||
return this.http.authedRequest(Method.Post, "/keys/signatures/upload", undefined, content, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
return this.http.authedRequest(Method.Post, "/keys/signatures/upload", undefined, content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,12 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IClearEvent, MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IEventDecryptionResult } from "../@types/crypto";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
@@ -47,9 +46,9 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
/**
|
||||
* Get the verification level for a given user
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
*
|
||||
* @deprecated Superceded by {@link CryptoApi#getUserVerificationStatus}.
|
||||
*/
|
||||
checkUserTrust(userId: string): UserTrustLevel;
|
||||
|
||||
@@ -71,7 +70,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @returns a promise which resolves once we have finished decrypting.
|
||||
* Rejects with an error if there is a problem decrypting the event.
|
||||
*/
|
||||
decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
||||
decryptEvent(event: MatrixEvent): Promise<EventDecryptionResult>;
|
||||
|
||||
/**
|
||||
* Get information about the encryption of an event
|
||||
@@ -88,6 +87,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
* @deprecated Prefer {@link CryptoApi#userHasCrossSigningKeys}
|
||||
*/
|
||||
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
||||
|
||||
@@ -181,3 +181,31 @@ export interface OnSyncCompletedData {
|
||||
export interface CheckOwnCrossSigningTrustOpts {
|
||||
allowPrivateKeyRequests?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to {@link CryptoBackend.decryptEvent}
|
||||
*/
|
||||
export interface EventDecryptionResult {
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*/
|
||||
clearEvent: IClearEvent;
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
* See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
*/
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
/**
|
||||
* Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
|
||||
*/
|
||||
senderCurve25519Key?: string;
|
||||
/**
|
||||
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
/**
|
||||
* The sender doesn't authorize the unverified devices to decrypt his messages
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ export function getHttpUriForMxc(
|
||||
}
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
let prefix = "/_matrix/media/v3/download/";
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (width) {
|
||||
@@ -64,7 +64,7 @@ export function getHttpUriForMxc(
|
||||
if (Object.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
prefix = "/_matrix/media/v3/thumbnail/";
|
||||
}
|
||||
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
|
||||
+110
-12
@@ -20,7 +20,7 @@ import { DeviceMap } from "./models/device";
|
||||
import { UIAuthCallback } from "./interactive-auth";
|
||||
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
|
||||
import { VerificationRequest } from "./crypto-api/verification";
|
||||
import { BackupTrustInfo, KeyBackupInfo } from "./crypto-api/keybackup";
|
||||
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
|
||||
import { ISignatures } from "./@types/signed";
|
||||
|
||||
/**
|
||||
@@ -38,16 +38,6 @@ export interface CryptoApi {
|
||||
*/
|
||||
globalBlacklistUnverifiedDevices: boolean;
|
||||
|
||||
/**
|
||||
* Checks if the user has previously published cross-signing keys
|
||||
*
|
||||
* This means downloading the devicelist for the user and checking if the list includes
|
||||
* the cross-signing pseudo-device.
|
||||
*
|
||||
* @returns true if the user has previously published cross-signing keys
|
||||
*/
|
||||
userHasCrossSigningKeys(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
@@ -88,6 +78,22 @@ export interface CryptoApi {
|
||||
*/
|
||||
importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if the given user has published cross-signing keys.
|
||||
*
|
||||
* - If the user is tracked, a `/keys/query` request is made to update locally the cross signing keys.
|
||||
* - If the user is not tracked locally and downloadUncached is set to true,
|
||||
* a `/keys/query` request is made to the server to retrieve the cross signing keys.
|
||||
* - Otherwise, return false
|
||||
*
|
||||
* @param userId - the user ID to check. Defaults to the local user.
|
||||
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
||||
* currently tracking. Defaults to false, in which case `false` will be returned for such users.
|
||||
*
|
||||
* @returns true if the cross signing keys are available.
|
||||
*/
|
||||
userHasCrossSigningKeys(userId?: string, downloadUncached?: boolean): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
@@ -126,6 +132,14 @@ export interface CryptoApi {
|
||||
*/
|
||||
getTrustCrossSignedDevices(): boolean;
|
||||
|
||||
/**
|
||||
* Get the verification status of a given user.
|
||||
*
|
||||
* @param userId - The ID of the user to check.
|
||||
*
|
||||
*/
|
||||
getUserVerificationStatus(userId: string): Promise<UserVerificationStatus>;
|
||||
|
||||
/**
|
||||
* Get the verification status of a given device.
|
||||
*
|
||||
@@ -164,6 +178,8 @@ export interface CryptoApi {
|
||||
* return true.
|
||||
*
|
||||
* @returns True if cross-signing is ready to be used on this device
|
||||
*
|
||||
* @throws May throw {@link ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
|
||||
*/
|
||||
isCrossSigningReady(): Promise<boolean>;
|
||||
|
||||
@@ -228,7 +244,10 @@ export interface CryptoApi {
|
||||
/**
|
||||
* Get the status of our cross-signing keys.
|
||||
*
|
||||
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and whether the private keys are in secret storage.
|
||||
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and
|
||||
* whether the private keys are in secret storage.
|
||||
*
|
||||
* @throws May throw {@link ClientStoppedError} if the `MatrixClient` is stopped before or during the call.
|
||||
*/
|
||||
getCrossSigningStatus(): Promise<CrossSigningStatus>;
|
||||
|
||||
@@ -281,6 +300,16 @@ export interface CryptoApi {
|
||||
*/
|
||||
findVerificationRequestDMInProgress(roomId: string, userId?: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Request a key verification from another user, using a DM.
|
||||
*
|
||||
* @param userId - the user to request verification with.
|
||||
* @param roomId - the room to use for verification.
|
||||
*
|
||||
* @returns resolves to a VerificationRequest when the request has been sent to the other party.
|
||||
*/
|
||||
requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest>;
|
||||
|
||||
/**
|
||||
* Send a verification request to our other devices.
|
||||
*
|
||||
@@ -350,6 +379,35 @@ export interface CryptoApi {
|
||||
* @param info - key backup info dict from {@link MatrixClient#getKeyBackupVersion}.
|
||||
*/
|
||||
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
|
||||
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* Fetches the current backup information from the server. If there is a backup, and it is trusted, starts
|
||||
* backing up to it; otherwise, disables backups.
|
||||
*
|
||||
* @returns `null` if there is no backup on the server. Otherwise, data on the backup as returned by the server,
|
||||
* and trust information (as returned by {@link isKeyBackupTrusted}).
|
||||
*/
|
||||
checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null>;
|
||||
|
||||
/**
|
||||
* Creates a new key backup version.
|
||||
*
|
||||
* If there are existing backups they will be replaced.
|
||||
*
|
||||
* The decryption key will be saved in Secret Storage (the {@link SecretStorageCallbacks.getSecretStorageKey} Crypto
|
||||
* callback will be called)
|
||||
* and the backup engine will be started.
|
||||
*/
|
||||
resetKeyBackup(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes the given key backup.
|
||||
*
|
||||
* @param version - The backup version to delete.
|
||||
*/
|
||||
deleteKeyBackupVersion(version: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,6 +424,46 @@ export interface BootstrapCrossSigningOpts {
|
||||
authUploadDeviceSigningKeys?: UIAuthCallback<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserVerificationStatus {
|
||||
public constructor(
|
||||
private readonly crossSigningVerified: boolean,
|
||||
private readonly crossSigningVerifiedBefore: boolean,
|
||||
private readonly tofu: boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns true if this user is verified via any means
|
||||
*/
|
||||
public isVerified(): boolean {
|
||||
return this.isCrossSigningVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this user is verified via cross signing
|
||||
*/
|
||||
public isCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we ever verified this user before (at least for
|
||||
* the history of verifications observed by this device).
|
||||
*/
|
||||
public wasCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerifiedBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this user's key is trusted on first use
|
||||
*/
|
||||
public isTofu(): boolean {
|
||||
return this.tofu;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceVerificationStatus {
|
||||
/**
|
||||
* True if this device has been signed by its owner (and that signature verified).
|
||||
|
||||
@@ -60,3 +60,11 @@ export interface BackupTrustInfo {
|
||||
*/
|
||||
readonly matchesDecryptionKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
export interface KeyBackupCheck {
|
||||
backupInfo: KeyBackupInfo;
|
||||
trustInfo: BackupTrustInfo;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
import { DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
|
||||
|
||||
// backwards-compatibility re-exports
|
||||
export { UserTrustLevel };
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -587,46 +590,6 @@ export enum CrossSigningLevel {
|
||||
SELF_SIGNING = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserTrustLevel {
|
||||
public constructor(
|
||||
private readonly crossSigningVerified: boolean,
|
||||
private readonly crossSigningVerifiedBefore: boolean,
|
||||
private readonly tofu: boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns true if this user is verified via any means
|
||||
*/
|
||||
public isVerified(): boolean {
|
||||
return this.isCrossSigningVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this user is verified via cross signing
|
||||
*/
|
||||
public isCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we ever verified this user before (at least for
|
||||
* the history of verifications observed by this device).
|
||||
*/
|
||||
public wasCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerifiedBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this user's key is trusted on first use
|
||||
*/
|
||||
public isTofu(): boolean {
|
||||
return this.tofu;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a device.
|
||||
*
|
||||
|
||||
@@ -228,6 +228,8 @@ export class EncryptionSetupOperation {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
}
|
||||
// tell the backup manager to re-check the keys now that they have been (maybe) updated
|
||||
await crypto.backupManager.checkKeyBackup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-14
@@ -25,7 +25,7 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from "./CrossSigning";
|
||||
import { keyFromPassphrase } from "./key_passphrase";
|
||||
import { safeSet, sleep } from "../utils";
|
||||
import { encodeUri, safeSet, sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { encodeRecoveryKey } from "./recoverykey";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { CryptoEvent } from "./index";
|
||||
import { crypto } from "./crypto";
|
||||
import { HTTPError, MatrixError } from "../http-api";
|
||||
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api";
|
||||
import { BackupTrustInfo } from "../crypto-api/keybackup";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
@@ -224,6 +224,33 @@ export class BackupManager {
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all key backups.
|
||||
*
|
||||
* Will call the API to delete active backup until there is no more present.
|
||||
*/
|
||||
public async deleteAllKeyBackupVersions(): Promise<void> {
|
||||
// there could be several backup versions, delete all to be safe.
|
||||
let current = (await this.baseApis.getKeyBackupVersion())?.version ?? null;
|
||||
while (current != null) {
|
||||
await this.deleteKeyBackupVersion(current);
|
||||
this.disableKeyBackup();
|
||||
current = (await this.baseApis.getKeyBackupVersion())?.version ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given key backup.
|
||||
*
|
||||
* @param version - The backup version to delete.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
const path = encodeUri("/room_keys/version/$version", { $version: version });
|
||||
await this.baseApis.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
@@ -333,7 +360,7 @@ export class BackupManager {
|
||||
};
|
||||
|
||||
if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
logger.info(`Key backup is absent or missing required data: ${JSON.stringify(backupInfo)}`);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -440,6 +467,7 @@ export class BackupManager {
|
||||
* @param maxDelay - Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
|
||||
logger.debug(`Key backup: scheduleKeyBackupSend currentSending:${this.sendingBackups} delay:${maxDelay}`);
|
||||
if (this.sendingBackups) return;
|
||||
|
||||
this.sendingBackups = true;
|
||||
@@ -452,6 +480,7 @@ export class BackupManager {
|
||||
await sleep(delay);
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send aborted, client stopped");
|
||||
this.sendingBackups = false;
|
||||
return;
|
||||
}
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
@@ -463,24 +492,26 @@ export class BackupManager {
|
||||
const numBackedUp = await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
this.sendingBackups = false;
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.log("Key backup request failed", err);
|
||||
if ((<MatrixError>err).data) {
|
||||
if (
|
||||
(<MatrixError>err).data.errcode == "M_NOT_FOUND" ||
|
||||
(<MatrixError>err).data.errcode == "M_WRONG_ROOM_KEYS_VERSION"
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
if (err instanceof MatrixError) {
|
||||
const errCode = err.data.errcode;
|
||||
if (errCode == "M_NOT_FOUND" || errCode == "M_WRONG_ROOM_KEYS_VERSION") {
|
||||
// Set to false now as `checkKeyBackup` might schedule a backupsend before this one ends.
|
||||
this.sendingBackups = false;
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (<MatrixError>err).data.errcode!);
|
||||
throw err;
|
||||
this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, errCode);
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
// This call might restart the backup loop if new backup version is trusted
|
||||
await this.checkKeyBackup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,10 +522,14 @@ export class BackupManager {
|
||||
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send loop aborted, client stopped");
|
||||
this.sendingBackups = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
} catch (err) {
|
||||
// No one actually checks errors on this promise, it's spawned internally.
|
||||
// Just log, apps/client should use events to check status
|
||||
logger.log(`Backup loop failed ${err}`);
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
}
|
||||
|
||||
+69
-4
@@ -92,11 +92,13 @@ import {
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeysOpts,
|
||||
KeyBackupCheck,
|
||||
KeyBackupInfo,
|
||||
VerificationRequest as CryptoApiVerificationRequest,
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
import { ClientPrefix, Method } from "../http-api";
|
||||
|
||||
/* re-exports for backwards compatibility */
|
||||
export type {
|
||||
@@ -699,9 +701,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async userHasCrossSigningKeys(): Promise<boolean> {
|
||||
await this.downloadKeys([this.userId]);
|
||||
return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null;
|
||||
public async userHasCrossSigningKeys(userId = this.userId): Promise<boolean> {
|
||||
await this.downloadKeys([userId]);
|
||||
return this.deviceList.getStoredCrossSigningForUser(userId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1087,7 +1089,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// and want to write to the local secretStorage object
|
||||
{ secureSecretStorage: false },
|
||||
);
|
||||
// write the key ourselves to 4S
|
||||
// write the key to 4S
|
||||
const privateKey = decodeRecoveryKey(info.recovery_key);
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
|
||||
|
||||
@@ -1144,6 +1146,48 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
logger.log("Secure Secret Storage ready");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#resetKeyBackup}.
|
||||
*/
|
||||
public async resetKeyBackup(): Promise<void> {
|
||||
// Delete existing ones
|
||||
// There is no use case for having several key backup version live server side.
|
||||
// Even if not deleted it would be lost as the key to restore is lost.
|
||||
// There should be only one backup at a time.
|
||||
await this.backupManager.deleteAllKeyBackupVersions();
|
||||
|
||||
const info = await this.backupManager.prepareKeyBackupVersion();
|
||||
|
||||
await this.signObject(info.auth_data);
|
||||
|
||||
// add new key backup
|
||||
const { version } = await this.baseApis.http.authedRequest<{ version: string }>(
|
||||
Method.Post,
|
||||
"/room_keys/version",
|
||||
undefined,
|
||||
info,
|
||||
{
|
||||
prefix: ClientPrefix.V3,
|
||||
},
|
||||
);
|
||||
|
||||
logger.log(`Created backup version ${version}`);
|
||||
|
||||
// write the key to 4S
|
||||
const privateKey = info.privateKey;
|
||||
await this.secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
|
||||
await this.storeSessionBackupPrivateKey(privateKey);
|
||||
|
||||
await this.backupManager.checkAndStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
await this.backupManager.deleteKeyBackupVersion(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#addKey}.
|
||||
*/
|
||||
@@ -1304,6 +1348,20 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return backupTrustInfoFromLegacyTrustInfo(trustInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* Implementation of {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
|
||||
const checkResult = await this.backupManager.checkKeyBackup();
|
||||
if (!checkResult || !checkResult.backupInfo) return null;
|
||||
return {
|
||||
backupInfo: checkResult.backupInfo,
|
||||
trustInfo: backupTrustInfoFromLegacyTrustInfo(checkResult.trustInfo),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given cross-signing private key matches a given public key.
|
||||
* This can be used by the getCrossSigningKey callback to verify that the
|
||||
@@ -1509,6 +1567,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.crossSigningInfo.checkUserTrust(userCrossSigning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.getUserVerificationStatus}.
|
||||
*/
|
||||
public async getUserVerificationStatus(userId: string): Promise<UserTrustLevel> {
|
||||
return this.checkUserTrust(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
|
||||
@@ -51,3 +51,17 @@ export class KeySignatureUploadError extends Error {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It is invalid to call most methods once {@link MatrixClient#stopClient} has been called.
|
||||
*
|
||||
* This error will be thrown if you attempt to do so.
|
||||
*
|
||||
* {@link MatrixClient#stopClient} itself is an exception to this: it may safely be called multiple times on the same
|
||||
* instance.
|
||||
*/
|
||||
export class ClientStoppedError extends Error {
|
||||
public constructor() {
|
||||
super("MatrixClient has been stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
});
|
||||
};
|
||||
|
||||
const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
|
||||
const url = this.getUrl("/upload", undefined, MediaPrefix.V3);
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
url.searchParams.set("filename", encodeURIComponent(fileName));
|
||||
@@ -139,7 +139,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
this.authedRequest<UploadResponse>(Method.Post, "/upload", queryParams, file, {
|
||||
prefix: MediaPrefix.R0,
|
||||
prefix: MediaPrefix.V3,
|
||||
headers,
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
@@ -182,7 +182,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
public getContentUri(): IContentUri {
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: MediaPrefix.R0 + "/upload",
|
||||
path: MediaPrefix.V3 + "/upload",
|
||||
params: {
|
||||
access_token: this.opts.accessToken!,
|
||||
},
|
||||
|
||||
@@ -16,11 +16,7 @@ limitations under the License.
|
||||
|
||||
export enum ClientPrefix {
|
||||
/**
|
||||
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
|
||||
*/
|
||||
R0 = "/_matrix/client/r0",
|
||||
/**
|
||||
* A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
|
||||
* A constant representing the URI path for Client-Server API endpoints versioned at v1.
|
||||
*/
|
||||
V1 = "/_matrix/client/v1",
|
||||
/**
|
||||
@@ -42,7 +38,11 @@ export enum IdentityPrefix {
|
||||
|
||||
export enum MediaPrefix {
|
||||
/**
|
||||
* URI path for the media repo API
|
||||
* A constant representing the URI path for Client-Server API Media endpoints versioned at v1.
|
||||
*/
|
||||
R0 = "/_matrix/media/r0",
|
||||
V1 = "/_matrix/media/v3",
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API Media endpoints versioned at v3.
|
||||
*/
|
||||
V3 = "/_matrix/media/v3",
|
||||
}
|
||||
|
||||
@@ -345,10 +345,8 @@ export class InteractiveAuth<T> {
|
||||
sid: this.emailSid,
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
||||
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
authDict = {
|
||||
type: EMAIL_STAGE_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
|
||||
@@ -50,6 +50,7 @@ export * from "./service-types";
|
||||
export * from "./store/memory";
|
||||
export * from "./store/indexeddb";
|
||||
export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/localStorage-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export type { OutgoingRoomKeyRequest } from "./crypto/store/base";
|
||||
export * from "./content-repo";
|
||||
@@ -59,6 +60,18 @@ export * from "./@types/PushRules";
|
||||
export * from "./@types/partials";
|
||||
export * from "./@types/requests";
|
||||
export * from "./@types/search";
|
||||
export * from "./@types/beacon";
|
||||
export * from "./@types/topic";
|
||||
export * from "./@types/location";
|
||||
export * from "./@types/threepids";
|
||||
export * from "./@types/auth";
|
||||
export * from "./@types/polls";
|
||||
export * from "./@types/local_notifications";
|
||||
export * from "./@types/registration";
|
||||
export * from "./@types/read_receipts";
|
||||
export * from "./@types/crypto";
|
||||
export * from "./@types/extensible_events";
|
||||
export * from "./@types/IIdentityServerProvider";
|
||||
export * from "./models/room-summary";
|
||||
export * from "./models/event-status";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
|
||||
@@ -1684,7 +1684,6 @@ const REDACT_KEEP_KEYS = new Set([
|
||||
// a map from state event type to the .content keys we keep when an event is redacted
|
||||
const REDACT_KEEP_CONTENT_MAP: Record<string, Record<string, 1>> = {
|
||||
[EventType.RoomMember]: { membership: 1 },
|
||||
[EventType.RoomCreate]: { creator: 1 },
|
||||
[EventType.RoomJoinRules]: { join_rule: 1 },
|
||||
[EventType.RoomPowerLevels]: {
|
||||
ban: 1,
|
||||
|
||||
+12
-1
@@ -146,6 +146,7 @@ export enum RoomEvent {
|
||||
CurrentStateUpdated = "Room.CurrentStateUpdated",
|
||||
HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline",
|
||||
UnreadNotifications = "Room.UnreadNotifications",
|
||||
Summary = "Room.Summary",
|
||||
}
|
||||
|
||||
export type RoomEmittedEvents =
|
||||
@@ -290,6 +291,14 @@ export type RoomEventHandlerMap = {
|
||||
[RoomEvent.HistoryImportedWithinTimeline]: (markerEvent: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void;
|
||||
[RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
|
||||
/**
|
||||
* Fires when a new room summary is returned by `/sync`.
|
||||
*
|
||||
* See https://spec.matrix.org/v1.8/client-server-api/#_matrixclientv3sync_roomsummary
|
||||
* for full details
|
||||
* @param summary - the room summary object
|
||||
*/
|
||||
[RoomEvent.Summary]: (summary: IRoomSummary) => void;
|
||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||
/**
|
||||
* Fires when a new poll instance is added to the room state
|
||||
@@ -569,7 +578,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
public getCreator(): string | null {
|
||||
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
return createEvent?.getContent()["creator"] ?? null;
|
||||
return createEvent?.getSender() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1499,6 +1508,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return userId !== this.myUserId;
|
||||
});
|
||||
}
|
||||
|
||||
this.emit(RoomEvent.Summary, summary);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ export class CrossSigningIdentity {
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
private readonly secretStorage: ServerSideSecretStorage,
|
||||
/** Called if the cross signing keys are imported from the secret storage */
|
||||
private readonly onCrossSigningKeysImport: () => void,
|
||||
private readonly onCrossSigningKeysImport: () => Promise<void>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -96,7 +96,7 @@ export class CrossSigningIdentity {
|
||||
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
|
||||
this.onCrossSigningKeysImport();
|
||||
await this.onCrossSigningKeysImport();
|
||||
}
|
||||
|
||||
// TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the
|
||||
|
||||
@@ -75,7 +75,12 @@ export class OutgoingRequestProcessor {
|
||||
} else if (msg instanceof SignatureUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysBackupRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
|
||||
resp = await this.rawJsonRequest(
|
||||
Method.Put,
|
||||
"/_matrix/client/v3/room_keys/keys",
|
||||
{ version: msg.version },
|
||||
msg.body,
|
||||
);
|
||||
} else if (msg instanceof ToDeviceRequest) {
|
||||
const path =
|
||||
`/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` +
|
||||
@@ -83,7 +88,7 @@ export class OutgoingRequestProcessor {
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
} else if (msg instanceof RoomMessageRequest) {
|
||||
const path =
|
||||
`/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` +
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(msg.room_id)}/send/` +
|
||||
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
} else if (msg instanceof SigningKeysUploadRequest) {
|
||||
@@ -100,7 +105,20 @@ export class OutgoingRequestProcessor {
|
||||
}
|
||||
|
||||
if (msg.id) {
|
||||
await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
|
||||
try {
|
||||
await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
|
||||
} catch (e) {
|
||||
// Ignore errors which are caused by the olmMachine having been freed. The exact error message depends
|
||||
// on whether we are using a release or develop build of rust-sdk-crypto-wasm.
|
||||
if (
|
||||
e instanceof Error &&
|
||||
(e.message === "Attempt to use a moved value" || e.message === "null pointer passed to rust")
|
||||
) {
|
||||
logger.log(`Ignoring error '${e.message}': client is likely shutting down`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+332
-5
@@ -17,20 +17,64 @@ limitations under the License.
|
||||
import { OlmMachine, SignatureVerification } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { BackupTrustInfo, Curve25519AuthData, KeyBackupInfo } from "../crypto-api/keybackup";
|
||||
import { BackupTrustInfo, Curve25519AuthData, KeyBackupCheck, KeyBackupInfo } from "../crypto-api/keybackup";
|
||||
import { logger } from "../logger";
|
||||
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
|
||||
import { CryptoEvent } from "../crypto";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { encodeUri } from "../utils";
|
||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { sleep } from "../utils";
|
||||
|
||||
/** Authentification of the backup info, depends on algorithm */
|
||||
type AuthData = KeyBackupInfo["auth_data"];
|
||||
|
||||
/**
|
||||
* Holds information of a created keybackup.
|
||||
* Useful to get the generated private key material and save it securely somewhere.
|
||||
*/
|
||||
interface KeyBackupCreationInfo {
|
||||
version: string;
|
||||
algorithm: string;
|
||||
authData: AuthData;
|
||||
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class RustBackupManager {
|
||||
public constructor(private readonly olmMachine: OlmMachine) {}
|
||||
export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents, RustBackupCryptoEventMap> {
|
||||
/** Have we checked if there is a backup on the server which we can use */
|
||||
private checkedForBackup = false;
|
||||
private activeBackupVersion: string | null = null;
|
||||
private stopped = false;
|
||||
|
||||
/** whether {@link backupKeysLoop} is currently running */
|
||||
private backupKeysLoopRunning = false;
|
||||
|
||||
public constructor(
|
||||
private readonly olmMachine: OlmMachine,
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the RustBackupManager to stop.
|
||||
* The RustBackupManager is scheduling background uploads of keys to the backup, this
|
||||
* call allows to cancel the process when the client is stoppped.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backup version we are currently backing up to, if any
|
||||
*/
|
||||
public async getActiveBackupVersion(): Promise<string | null> {
|
||||
// TODO stub
|
||||
return null;
|
||||
if (!this.olmMachine.isBackupEnabled()) return null;
|
||||
return this.activeBackupVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,4 +96,287 @@ export class RustBackupManager {
|
||||
trusted: signatureVerification.trusted(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-check the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* @param force - whether we should force a re-check even if one has already happened.
|
||||
*/
|
||||
public checkKeyBackupAndEnable(force: boolean): Promise<KeyBackupCheck | null> {
|
||||
if (!force && this.checkedForBackup) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// make sure there is only one check going on at a time
|
||||
if (!this.keyBackupCheckInProgress) {
|
||||
this.keyBackupCheckInProgress = this.doCheckKeyBackup().finally(() => {
|
||||
this.keyBackupCheckInProgress = null;
|
||||
});
|
||||
}
|
||||
return this.keyBackupCheckInProgress;
|
||||
}
|
||||
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
|
||||
|
||||
/** Helper for `checkKeyBackup` */
|
||||
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
|
||||
logger.log("Checking key backup status...");
|
||||
let backupInfo: KeyBackupInfo | null = null;
|
||||
try {
|
||||
backupInfo = await this.requestKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.warn("Error checking for active key backup", e);
|
||||
return null;
|
||||
}
|
||||
this.checkedForBackup = true;
|
||||
|
||||
if (backupInfo && !backupInfo.version) {
|
||||
logger.warn("active backup lacks a useful 'version'; ignoring it");
|
||||
}
|
||||
|
||||
const activeVersion = await this.getActiveBackupVersion();
|
||||
|
||||
if (!backupInfo) {
|
||||
if (activeVersion !== null) {
|
||||
logger.log("No key backup present on server: disabling key backup");
|
||||
await this.disableKeyBackup();
|
||||
} else {
|
||||
logger.log("No key backup present on server: not enabling key backup");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (!trustInfo.trusted) {
|
||||
if (activeVersion !== null) {
|
||||
logger.log("Key backup present on server but not trusted: disabling key backup");
|
||||
await this.disableKeyBackup();
|
||||
} else {
|
||||
logger.log("Key backup present on server but not trusted: not enabling key backup");
|
||||
}
|
||||
} else {
|
||||
if (activeVersion === null) {
|
||||
logger.log(`Found usable key backup v${backupInfo.version}: enabling key backups`);
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
} else if (activeVersion !== backupInfo.version) {
|
||||
logger.log(`On backup version ${activeVersion} but found version ${backupInfo.version}: switching.`);
|
||||
// This will remove any pending backup request, remove the backup key and reset the backup state of each room key we have.
|
||||
await this.disableKeyBackup();
|
||||
// Enabling will now trigger re-upload of all the keys
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
} else {
|
||||
logger.log(`Backup version ${backupInfo.version} still current`);
|
||||
}
|
||||
}
|
||||
return { backupInfo, trustInfo };
|
||||
}
|
||||
|
||||
private async enableKeyBackup(backupInfo: KeyBackupInfo): Promise<void> {
|
||||
// we know for certain it must be a Curve25519 key, because we have verified it and only Curve25519
|
||||
// keys can be verified.
|
||||
//
|
||||
// we also checked it has a valid `version`.
|
||||
await this.olmMachine.enableBackupV1(
|
||||
(backupInfo.auth_data as Curve25519AuthData).public_key,
|
||||
backupInfo.version!,
|
||||
);
|
||||
this.activeBackupVersion = backupInfo.version!;
|
||||
|
||||
this.emit(CryptoEvent.KeyBackupStatus, true);
|
||||
|
||||
this.backupKeysLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the backup key loop if there is an active trusted backup.
|
||||
* Doesn't try to check the backup server side. To be called when a new
|
||||
* megolm key is known locally.
|
||||
*/
|
||||
public async maybeUploadKey(): Promise<void> {
|
||||
if (this.activeBackupVersion != null) {
|
||||
this.backupKeysLoop();
|
||||
}
|
||||
}
|
||||
|
||||
private async disableKeyBackup(): Promise<void> {
|
||||
await this.olmMachine.disableBackup();
|
||||
this.activeBackupVersion = null;
|
||||
this.emit(CryptoEvent.KeyBackupStatus, false);
|
||||
}
|
||||
|
||||
private async backupKeysLoop(maxDelay = 10000): Promise<void> {
|
||||
if (this.backupKeysLoopRunning) {
|
||||
logger.log(`Backup loop already running`);
|
||||
return;
|
||||
}
|
||||
this.backupKeysLoopRunning = true;
|
||||
|
||||
logger.log(`Starting loop for ${this.activeBackupVersion}.`);
|
||||
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay);
|
||||
|
||||
try {
|
||||
let numFailures = 0; // number of consecutive network failures for exponential backoff
|
||||
|
||||
while (!this.stopped) {
|
||||
// Get a batch of room keys to upload
|
||||
const request: RustSdkCryptoJs.KeysBackupRequest | null = await this.olmMachine.backupRoomKeys();
|
||||
|
||||
if (!request || this.stopped || !this.activeBackupVersion) {
|
||||
logger.log(`Ending loop for ${this.activeBackupVersion}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
numFailures = 0;
|
||||
|
||||
const keyCount: RustSdkCryptoJs.RoomKeyCounts = await this.olmMachine.roomKeyCounts();
|
||||
const remaining = keyCount.total - keyCount.backedUp;
|
||||
this.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.error("Error processing backup request for rust crypto-sdk", err);
|
||||
if (err instanceof MatrixError) {
|
||||
const errCode = err.data.errcode;
|
||||
if (errCode == "M_NOT_FOUND" || errCode == "M_WRONG_ROOM_KEYS_VERSION") {
|
||||
await this.disableKeyBackup();
|
||||
this.emit(CryptoEvent.KeyBackupFailed, err.data.errcode!);
|
||||
// There was an active backup and we are out of sync with the server
|
||||
// force a check server side
|
||||
this.backupKeysLoopRunning = false;
|
||||
this.checkKeyBackupAndEnable(true);
|
||||
return;
|
||||
} else if (errCode == "M_LIMIT_EXCEEDED") {
|
||||
// wait for that and then continue?
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
sleep(waitTime);
|
||||
continue;
|
||||
} // else go to the normal backoff
|
||||
}
|
||||
}
|
||||
|
||||
// Some other errors (mx, network, or CORS or invalid urls?) anyhow backoff
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.backupKeysLoopRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current key backup from the server
|
||||
*
|
||||
* @returns Information object from API or null if there is no active backup.
|
||||
*/
|
||||
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
|
||||
try {
|
||||
return await this.http.authedRequest<KeyBackupInfo>(
|
||||
Method.Get,
|
||||
"/room_keys/version",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
prefix: ClientPrefix.V3,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if ((<MatrixError>e).errcode === "M_NOT_FOUND") {
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new key backup by generating a new random private key.
|
||||
*
|
||||
* If there is an existing backup server side it will be deleted and replaced
|
||||
* by the new one.
|
||||
*
|
||||
* @param signObject - Method that should sign the backup with existing device and
|
||||
* existing identity.
|
||||
* @returns a KeyBackupCreationInfo - All information related to the backup.
|
||||
*/
|
||||
public async setupKeyBackup(signObject: (authData: AuthData) => Promise<void>): Promise<KeyBackupCreationInfo> {
|
||||
// Clean up any existing backup
|
||||
await this.deleteAllKeyBackupVersions();
|
||||
|
||||
const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey();
|
||||
const pubKey = randomKey.megolmV1PublicKey;
|
||||
|
||||
const authData = { public_key: pubKey.publicKeyBase64 };
|
||||
|
||||
await signObject(authData);
|
||||
|
||||
const res = await this.http.authedRequest<{ version: string }>(
|
||||
Method.Post,
|
||||
"/room_keys/version",
|
||||
undefined,
|
||||
{
|
||||
algorithm: pubKey.algorithm,
|
||||
auth_data: authData,
|
||||
},
|
||||
{
|
||||
prefix: ClientPrefix.V3,
|
||||
},
|
||||
);
|
||||
|
||||
this.olmMachine.saveBackupDecryptionKey(randomKey, res.version);
|
||||
|
||||
return {
|
||||
version: res.version,
|
||||
algorithm: pubKey.algorithm,
|
||||
authData: authData,
|
||||
decryptionKey: randomKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all key backups.
|
||||
*
|
||||
* Will call the API to delete active backup until there is no more present.
|
||||
*/
|
||||
public async deleteAllKeyBackupVersions(): Promise<void> {
|
||||
// there could be several backup versions. Delete all to be safe.
|
||||
let current = (await this.requestKeyBackupVersion())?.version ?? null;
|
||||
while (current != null) {
|
||||
await this.deleteKeyBackupVersion(current);
|
||||
current = (await this.requestKeyBackupVersion())?.version ?? null;
|
||||
}
|
||||
|
||||
// XXX: Should this also update Secret Storage and delete any existing keys?
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given key backup.
|
||||
*
|
||||
* @param version - The backup version to delete.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
logger.debug(`deleteKeyBackupVersion v:${version}`);
|
||||
const path = encodeUri("/room_keys/version/$version", { $version: version });
|
||||
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type RustBackupCryptoEvents =
|
||||
| CryptoEvent.KeyBackupStatus
|
||||
| CryptoEvent.KeyBackupSessionsRemaining
|
||||
| CryptoEvent.KeyBackupFailed;
|
||||
|
||||
export type RustBackupCryptoEventMap = {
|
||||
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
|
||||
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
|
||||
[CryptoEvent.KeyBackupFailed]: (errCode: string) => void;
|
||||
};
|
||||
|
||||
+239
-39
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
@@ -25,7 +26,6 @@ import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
@@ -42,7 +42,9 @@ import {
|
||||
GeneratedSecretStorageKey,
|
||||
ImportRoomKeyProgressData,
|
||||
ImportRoomKeysOpts,
|
||||
KeyBackupCheck,
|
||||
KeyBackupInfo,
|
||||
UserVerificationStatus,
|
||||
VerificationRequest,
|
||||
} from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
@@ -54,14 +56,23 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||
import { crypto } from "../crypto/crypto";
|
||||
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||
import { EventType, MsgType } from "../@types/event";
|
||||
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||
import { EventType } from "../@types/event";
|
||||
import { CryptoEvent } from "../crypto";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { RustBackupManager } from "./backup";
|
||||
import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { randomString } from "../randomstring";
|
||||
import { ClientStoppedError } from "../errors";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
|
||||
|
||||
interface ISignableObject {
|
||||
signatures?: ISignatures;
|
||||
unsigned?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
*
|
||||
@@ -84,8 +95,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
private keyClaimManager: KeyClaimManager;
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
private crossSigningIdentity: CrossSigningIdentity;
|
||||
private readonly backupManager: RustBackupManager;
|
||||
|
||||
public readonly backupManager: RustBackupManager;
|
||||
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this);
|
||||
|
||||
public constructor(
|
||||
/** The `OlmMachine` from the underlying rust crypto sdk. */
|
||||
@@ -114,11 +126,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
this.eventDecryptor = new EventDecryptor(olmMachine);
|
||||
this.backupManager = new RustBackupManager(olmMachine);
|
||||
|
||||
this.backupManager = new RustBackupManager(olmMachine, http, this.outgoingRequestProcessor);
|
||||
this.reemitter.reEmit(this.backupManager, [
|
||||
CryptoEvent.KeyBackupStatus,
|
||||
CryptoEvent.KeyBackupSessionsRemaining,
|
||||
CryptoEvent.KeyBackupFailed,
|
||||
]);
|
||||
|
||||
// Fire if the cross signing keys are imported from the secret storage
|
||||
const onCrossSigningKeysImport = (): void => {
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(this.userId));
|
||||
const onCrossSigningKeysImport = async (): Promise<void> => {
|
||||
const newVerification = await this.getUserVerificationStatus(this.userId);
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, newVerification);
|
||||
};
|
||||
this.crossSigningIdentity = new CrossSigningIdentity(
|
||||
olmMachine,
|
||||
@@ -128,6 +147,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the OlmMachine only if {@link RustCrypto#stop} has not been called.
|
||||
*
|
||||
* This allows us to better handle race conditions where the client is stopped before or during a crypto API call.
|
||||
*
|
||||
* @throws ClientStoppedError if {@link RustCrypto#stop} has been called.
|
||||
*/
|
||||
private getOlmMachineOrThrow(): RustSdkCryptoJs.OlmMachine {
|
||||
if (this.stopped) {
|
||||
throw new ClientStoppedError();
|
||||
}
|
||||
return this.olmMachine;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CryptoBackend implementation
|
||||
@@ -143,6 +176,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
this.stopped = true;
|
||||
|
||||
this.keyClaimManager.stop();
|
||||
this.backupManager.stop();
|
||||
|
||||
// make sure we close() the OlmMachine; doing so means that all the Rust objects will be
|
||||
// cleaned up; in particular, the indexeddb connections will be closed, which means they
|
||||
@@ -192,9 +226,14 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
return ret as IEncryptedEventInfo;
|
||||
}
|
||||
|
||||
public checkUserTrust(userId: string): UserTrustLevel {
|
||||
// TODO
|
||||
return new UserTrustLevel(false, false, false);
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#checkUserTrust}.
|
||||
*
|
||||
* Stub for backwards compatibility.
|
||||
*
|
||||
*/
|
||||
public checkUserTrust(userId: string): UserVerificationStatus {
|
||||
return new UserVerificationStatus(false, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +257,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* The function is stub to keep the compatibility with the old crypto.
|
||||
* More information: https://github.com/vector-im/element-web/issues/25648
|
||||
*
|
||||
*
|
||||
* Implementation of {@link CryptoBackend#checkOwnCrossSigningTrust}
|
||||
*/
|
||||
public async checkOwnCrossSigningTrust(): Promise<void> {
|
||||
@@ -233,20 +271,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
|
||||
public globalBlacklistUnverifiedDevices = false;
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
|
||||
*/
|
||||
public async userHasCrossSigningKeys(): Promise<boolean> {
|
||||
const userId = new RustSdkCryptoJs.UserId(this.userId);
|
||||
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
|
||||
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
|
||||
*/
|
||||
const request = this.olmMachine.queryKeysForUsers([userId]);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
const userIdentity = await this.olmMachine.getIdentity(userId);
|
||||
return userIdentity !== undefined;
|
||||
}
|
||||
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
const encryptor = this.roomEncryptors[room.roomId];
|
||||
|
||||
@@ -278,6 +302,47 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
|
||||
*/
|
||||
public async userHasCrossSigningKeys(userId = this.userId, downloadUncached = false): Promise<boolean> {
|
||||
// TODO: could probably do with a more efficient way of doing this than returning the whole set and searching
|
||||
const rustTrackedUsers: Set<RustSdkCryptoJs.UserId> = await this.olmMachine.trackedUsers();
|
||||
let rustTrackedUser: RustSdkCryptoJs.UserId | undefined;
|
||||
for (const u of rustTrackedUsers) {
|
||||
if (userId === u.toString()) {
|
||||
rustTrackedUser = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rustTrackedUser !== undefined) {
|
||||
if (userId === this.userId) {
|
||||
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
|
||||
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
|
||||
*/
|
||||
const request = this.olmMachine.queryKeysForUsers([rustTrackedUser]);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
}
|
||||
const userIdentity = await this.olmMachine.getIdentity(rustTrackedUser);
|
||||
return userIdentity !== undefined;
|
||||
} else if (downloadUncached) {
|
||||
// Download the cross signing keys and check if the master key is available
|
||||
const keyResult = await this.downloadDeviceList(new Set([userId]));
|
||||
const keys = keyResult.master_keys?.[userId];
|
||||
|
||||
// No master key
|
||||
if (!keys) return false;
|
||||
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
return Boolean(Object.values(keys.keys)[0]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
@@ -422,6 +487,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getUserVerificationStatus}.
|
||||
*/
|
||||
public async getUserVerificationStatus(userId: string): Promise<UserVerificationStatus> {
|
||||
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(userId));
|
||||
if (userIdentity === undefined) {
|
||||
return new UserVerificationStatus(false, false, false);
|
||||
}
|
||||
return new UserVerificationStatus(userIdentity.isVerified(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isCrossSigningReady}
|
||||
*/
|
||||
@@ -502,6 +579,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
public async bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage,
|
||||
setupNewKeyBackup,
|
||||
}: CreateSecretStorageOpts = {}): Promise<void> {
|
||||
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
|
||||
// we don't want to create a new key
|
||||
@@ -545,6 +623,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
|
||||
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
|
||||
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
|
||||
|
||||
if (setupNewKeyBackup) {
|
||||
await this.resetKeyBackup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,16 +679,17 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||
*/
|
||||
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.olmMachine.getIdentity(
|
||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.getOlmMachineOrThrow().getIdentity(
|
||||
new RustSdkCryptoJs.UserId(this.userId),
|
||||
);
|
||||
|
||||
const publicKeysOnDevice =
|
||||
Boolean(userIdentity?.masterKey) &&
|
||||
Boolean(userIdentity?.selfSigningKey) &&
|
||||
Boolean(userIdentity?.userSigningKey);
|
||||
const privateKeysInSecretStorage = await secretStorageContainsCrossSigningKeys(this.secretStorage);
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus | null =
|
||||
await this.olmMachine.crossSigningStatus();
|
||||
await this.getOlmMachineOrThrow().crossSigningStatus();
|
||||
|
||||
return {
|
||||
publicKeysOnDevice,
|
||||
@@ -704,6 +787,62 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#requestVerificationDM}
|
||||
*/
|
||||
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||
const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
);
|
||||
|
||||
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
|
||||
|
||||
// Transform the verification methods into rust objects
|
||||
const methods = this._supportedVerificationMethods.map((method) =>
|
||||
verificationMethodIdentifierToMethod(method),
|
||||
);
|
||||
// Get the request content to send to the DM room
|
||||
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
|
||||
|
||||
// Send the request content to send to the DM room
|
||||
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
|
||||
|
||||
// Get a verification request
|
||||
const request: RustSdkCryptoJs.VerificationRequest = await userIdentity.requestVerification(
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
new RustSdkCryptoJs.EventId(eventId),
|
||||
methods,
|
||||
);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the verification content to a room
|
||||
* See https://spec.matrix.org/v1.7/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
*
|
||||
* Prefer to use {@link OutgoingRequestProcessor.makeOutgoingRequest} when dealing with {@link RustSdkCryptoJs.RoomMessageRequest}
|
||||
*
|
||||
* @param roomId - the targeted room
|
||||
* @param verificationEventContent - the request body.
|
||||
*
|
||||
* @returns the event id
|
||||
*/
|
||||
private async sendVerificationRequestContent(roomId: string, verificationEventContent: string): Promise<string> {
|
||||
const txId = randomString(32);
|
||||
// Send the verification request content to the DM room
|
||||
const { event_id: eventId } = await this.http.authedRequest<{ event_id: string }>(
|
||||
Method.Put,
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(txId)}`,
|
||||
undefined,
|
||||
verificationEventContent,
|
||||
{
|
||||
prefix: "",
|
||||
},
|
||||
);
|
||||
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The verification methods we offer to the other side during an interactive verification.
|
||||
*/
|
||||
@@ -819,6 +958,64 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
return await this.backupManager.isKeyBackupTrusted(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* Implementation of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
|
||||
return await this.backupManager.checkKeyBackupAndEnable(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
|
||||
*/
|
||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
||||
await this.backupManager.deleteKeyBackupVersion(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#resetKeyBackup}.
|
||||
*/
|
||||
public async resetKeyBackup(): Promise<void> {
|
||||
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
|
||||
|
||||
// we want to store the private key in 4S
|
||||
// need to check if 4S is set up?
|
||||
if (await this.secretStorageHasAESKey()) {
|
||||
await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64());
|
||||
}
|
||||
|
||||
// we can check and start async
|
||||
this.checkKeyBackupAndEnable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the given object with the current device and current identity (if available).
|
||||
* As defined in {@link https://spec.matrix.org/v1.8/appendices/#signing-json | Signing JSON}.
|
||||
*
|
||||
* @param obj - The object to sign
|
||||
*/
|
||||
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
|
||||
const sigs = new Map(Object.entries(obj.signatures || {}));
|
||||
const unsigned = obj.unsigned;
|
||||
|
||||
delete obj.signatures;
|
||||
delete obj.unsigned;
|
||||
|
||||
const userSignatures = sigs.get(this.userId) || {};
|
||||
|
||||
const canonalizedJson = anotherjson.stringify(obj);
|
||||
const signatures: RustSdkCryptoJs.Signatures = await this.olmMachine.sign(canonalizedJson);
|
||||
|
||||
const map = JSON.parse(signatures.asJSON());
|
||||
|
||||
sigs.set(this.userId, { ...userSignatures, ...map[this.userId] });
|
||||
|
||||
if (unsigned !== undefined) obj.unsigned = unsigned;
|
||||
obj.signatures = Object.fromEntries(sigs.entries());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
@@ -1008,9 +1205,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
for (const key of keys) {
|
||||
this.onRoomKeyUpdated(key);
|
||||
}
|
||||
this.backupManager.maybeUploadKey();
|
||||
}
|
||||
|
||||
private onRoomKeyUpdated(key: RustSdkCryptoJs.RoomKeyInfo): void {
|
||||
if (this.stopped) return;
|
||||
logger.debug(`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`);
|
||||
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
|
||||
if (pendingList.length === 0) return;
|
||||
@@ -1040,15 +1239,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* @param event - live event
|
||||
*/
|
||||
public async onLiveEventFromSync(event: MatrixEvent): Promise<void> {
|
||||
// Ignore state event
|
||||
if (event.isState()) return;
|
||||
// Ignore state event or remote echo
|
||||
// transaction_id is provided in case of remote echo {@link https://spec.matrix.org/v1.7/client-server-api/#local-echo}
|
||||
if (event.isState() || !!event.getUnsigned().transaction_id) return;
|
||||
|
||||
const processEvent = async (evt: MatrixEvent): Promise<void> => {
|
||||
// Process only key validation request
|
||||
if (
|
||||
evt.getType() === EventType.RoomMessage &&
|
||||
evt.getContent().msgtype === MsgType.KeyVerificationRequest
|
||||
) {
|
||||
// Process only verification event
|
||||
if (isVerificationEvent(event)) {
|
||||
await this.onKeyVerificationRequest(evt);
|
||||
}
|
||||
};
|
||||
@@ -1058,6 +1255,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
// 5 mins
|
||||
const TIMEOUT_DELAY = 5 * 60 * 1000;
|
||||
|
||||
// After 5mins, we are not expecting the event to be decrypted
|
||||
const timeoutId = setTimeout(() => event.off(MatrixEventEvent.Decrypted, onDecrypted), TIMEOUT_DELAY);
|
||||
|
||||
const onDecrypted = (decryptedEvent: MatrixEvent, error?: Error): void => {
|
||||
@@ -1067,7 +1265,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
event.off(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
processEvent(decryptedEvent);
|
||||
};
|
||||
// After 5mins, we are not expecting the event to be decrypted
|
||||
|
||||
event.on(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
} else {
|
||||
@@ -1231,7 +1428,10 @@ class EventDecryptor {
|
||||
}
|
||||
}
|
||||
|
||||
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged;
|
||||
type RustCryptoEvents =
|
||||
| CryptoEvent.VerificationRequestReceived
|
||||
| CryptoEvent.UserTrustStatusChanged
|
||||
| RustBackupCryptoEvents;
|
||||
|
||||
type RustCryptoEventMap = {
|
||||
/**
|
||||
@@ -1242,5 +1442,5 @@ type RustCryptoEventMap = {
|
||||
/**
|
||||
* Fires when the cross signing keys are imported during {@link CryptoApi#bootstrapCrossSigning}
|
||||
*/
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserTrustLevel) => void;
|
||||
};
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void;
|
||||
} & RustBackupCryptoEventMap;
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventType, MsgType } from "../@types/event";
|
||||
|
||||
/**
|
||||
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
|
||||
@@ -700,3 +702,28 @@ export function verificationMethodIdentifierToMethod(method: string): RustSdkCry
|
||||
}
|
||||
return meth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the event's type matches that of an in-room verification event
|
||||
*
|
||||
* @param event - MatrixEvent
|
||||
* @returns
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function isVerificationEvent(event: MatrixEvent): boolean {
|
||||
switch (event.getType()) {
|
||||
case EventType.KeyVerificationCancel:
|
||||
case EventType.KeyVerificationDone:
|
||||
case EventType.KeyVerificationMac:
|
||||
case EventType.KeyVerificationStart:
|
||||
case EventType.KeyVerificationKey:
|
||||
case EventType.KeyVerificationReady:
|
||||
case EventType.KeyVerificationAccept:
|
||||
return true;
|
||||
case EventType.RoomMessage:
|
||||
return event.getContent().msgtype === MsgType.KeyVerificationRequest;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-11
@@ -649,18 +649,11 @@ export class SyncApi {
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers) {
|
||||
debuglog("Checking server lazy load support...");
|
||||
const supported = await this.client.doesServerSupportLazyLoading();
|
||||
if (supported) {
|
||||
debuglog("Enabling lazy load on sync filter...");
|
||||
if (!this.opts.filter) {
|
||||
this.opts.filter = this.buildDefaultFilter();
|
||||
}
|
||||
this.opts.filter.setLazyLoadMembers(true);
|
||||
} else {
|
||||
debuglog("LL: lazy loading requested but not supported " + "by server, so disabling");
|
||||
this.opts.lazyLoadMembers = false;
|
||||
debuglog("Enabling lazy load on sync filter...");
|
||||
if (!this.opts.filter) {
|
||||
this.opts.filter = this.buildDefaultFilter();
|
||||
}
|
||||
this.opts.filter.setLazyLoadMembers(true);
|
||||
}
|
||||
// need to vape the store when enabling LL and wasn't enabled before
|
||||
debuglog("Checking whether lazy loading has changed in store...");
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The minimum Matrix specification version the js-sdk supports.
|
||||
*/
|
||||
export const MINIMUM_MATRIX_VERSION = "v1.1";
|
||||
+27
-4
@@ -2220,7 +2220,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
CallEvent.Error,
|
||||
new CallError(
|
||||
CallErrorCode.NoUserMedia,
|
||||
"Couldn't start capturing media! Is your microphone set up and " + "does this app have permission?",
|
||||
"Couldn't start capturing media! Is your microphone set up and does this app have permission?",
|
||||
err,
|
||||
),
|
||||
this,
|
||||
@@ -2228,6 +2228,22 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
|
||||
};
|
||||
|
||||
private placeCallFailed = (err: Error): void => {
|
||||
if (this.successor) {
|
||||
this.successor.placeCallFailed(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Call ${this.callId} placeCallWithCallFeeds() failed - ending call`, err);
|
||||
|
||||
this.emit(
|
||||
CallEvent.Error,
|
||||
new CallError(CallErrorCode.IceFailed, "Couldn't start call! Invalid ICE server configuration.", err),
|
||||
this,
|
||||
);
|
||||
this.terminate(CallParty.Local, CallErrorCode.IceFailed, false);
|
||||
};
|
||||
|
||||
private onIceConnectionStateChanged = (): void => {
|
||||
if (this.callHasEnded()) {
|
||||
return; // because ICE can still complete as we're ending the call
|
||||
@@ -2775,6 +2791,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
this.state = CallState.WaitLocalMedia;
|
||||
|
||||
let callFeed: CallFeed;
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
|
||||
|
||||
@@ -2783,7 +2800,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
const callFeed = new CallFeed({
|
||||
callFeed = new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
userId: this.client.getUserId()!,
|
||||
@@ -2793,11 +2810,17 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
});
|
||||
await this.placeCallWithCallFeeds([callFeed]);
|
||||
} catch (e) {
|
||||
this.getUserMediaFailed(<Error>e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.placeCallWithCallFeeds([callFeed]);
|
||||
} catch (e) {
|
||||
this.placeCallFailed(<Error>e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2834,7 +2857,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private createPeerConnection(): RTCPeerConnection {
|
||||
const pc = new window.RTCPeerConnection({
|
||||
iceTransportPolicy: this.forceTURN ? "relay" : undefined,
|
||||
iceServers: this.turnServers,
|
||||
iceServers: this.turnServers.length ? this.turnServers : undefined,
|
||||
iceCandidatePoolSize: this.client.iceCandidatePoolSize,
|
||||
bundlePolicy: "max-bundle",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user