Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b606d1e54b | |||
| cd7c519dc4 | |||
| 30dd28960c | |||
| 5b635df08d | |||
| 592c497902 | |||
| 8e3f2f3262 | |||
| 5751df1288 | |||
| 40a71101e2 | |||
| 3f095caf2d | |||
| 12a94bdd94 | |||
| 1c1ac137d3 | |||
| 89cabc4912 | |||
| 5be4548b3d | |||
| 09de76bd43 | |||
| 3a694f4998 | |||
| 3a8a1389f5 | |||
| c271e1533a | |||
| 722debe8f9 | |||
| 5165899e82 | |||
| 1828826661 | |||
| 24cee68fa2 | |||
| e645af1fc5 | |||
| de64779c27 | |||
| acbcb4658a | |||
| 815484b543 | |||
| 5a3d1a2a67 | |||
| 18626169e4 | |||
| e4a9f958a0 | |||
| ff29de743c | |||
| 5a68861418 | |||
| e285932776 | |||
| 2af0706b16 | |||
| 4382d2a425 | |||
| 9de4a057df | |||
| b703d4a2cc | |||
| d1dec4cd08 | |||
| 326a13bcfe | |||
| e8fb47fdca | |||
| bd66e3859d | |||
| 96e484a3fe | |||
| 3e646bdfa0 | |||
| 48c4127035 | |||
| f16a6bc654 | |||
| f884c78579 | |||
| 3c59476cf7 | |||
| c8f6c4dd0d | |||
| e8c89e9977 | |||
| df78d7cf67 | |||
| 80fec814a2 | |||
| 8b9672ba43 |
@@ -15,7 +15,7 @@ concurrency:
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.73.1
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.74.0
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
|
||||
@@ -19,7 +19,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.73.1
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.74.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
node: [18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -50,6 +50,9 @@ jobs:
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
Changes in [26.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.2.0) (2023-07-04)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. ([\#3189](https://github.com/matrix-org/matrix-js-sdk/issues/3189)).
|
||||
* ElementR: Add `CryptoApi#bootstrapSecretStorage` ([\#3483](https://github.com/matrix-org/matrix-js-sdk/pull/3483)). Contributed by @florianduros.
|
||||
* Deprecate `MatrixClient.findVerificationRequestDMInProgress`, `MatrixClient.getVerificationRequestsToDeviceInProgress`, and `MatrixClient.requestVerification`, in favour of methods in `CryptoApi`. ([\#3474](https://github.com/matrix-org/matrix-js-sdk/pull/3474)).
|
||||
* Introduce a new `Crypto.VerificationRequest` interface, and deprecate direct access to the old `VerificationRequest` class. Also deprecate some related classes that were exported from `src/crypto/verification/request/VerificationRequest` ([\#3449](https://github.com/matrix-org/matrix-js-sdk/pull/3449)).
|
||||
|
||||
## ✨ Features
|
||||
* OIDC: navigate to authorization endpoint ([\#3499](https://github.com/matrix-org/matrix-js-sdk/pull/3499)). Contributed by @kerryarchibald.
|
||||
* Support for interactive device verification in Element-R. ([\#3505](https://github.com/matrix-org/matrix-js-sdk/pull/3505)).
|
||||
* Support for interactive device verification in Element-R. ([\#3508](https://github.com/matrix-org/matrix-js-sdk/pull/3508)).
|
||||
* Support for interactive device verification in Element-R. ([\#3490](https://github.com/matrix-org/matrix-js-sdk/pull/3490)). Fixes vector-im/element-web#25316.
|
||||
* Element-R: Store cross signing keys in secret storage ([\#3498](https://github.com/matrix-org/matrix-js-sdk/pull/3498)). Contributed by @florianduros.
|
||||
* OIDC: add dynamic client registration util function ([\#3481](https://github.com/matrix-org/matrix-js-sdk/pull/3481)). Contributed by @kerryarchibald.
|
||||
* Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room ([\#3493](https://github.com/matrix-org/matrix-js-sdk/pull/3493)).
|
||||
* ElementR: Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation ([\#3472](https://github.com/matrix-org/matrix-js-sdk/pull/3472)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Aggregate relations regardless of whether event fits into the timeline ([\#3496](https://github.com/matrix-org/matrix-js-sdk/pull/3496)). Fixes vector-im/element-web#25596.
|
||||
* Fix bug where switching media caused media in subsequent calls to fail ([\#3489](https://github.com/matrix-org/matrix-js-sdk/pull/3489)).
|
||||
* Fix: remove polls from room state on redaction ([\#3475](https://github.com/matrix-org/matrix-js-sdk/pull/3475)). Fixes vector-im/element-web#25573. Contributed by @kerryarchibald.
|
||||
* Fix export type `GeneratedSecretStorageKey` ([\#3479](https://github.com/matrix-org/matrix-js-sdk/pull/3479)). Contributed by @florianduros.
|
||||
* Close IDB database before deleting it to prevent spurious unexpected close errors ([\#3478](https://github.com/matrix-org/matrix-js-sdk/pull/3478)). Fixes vector-im/element-web#25597.
|
||||
|
||||
Changes in [26.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0) (2023-06-20)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
## In a browser
|
||||
|
||||
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
|
||||
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
`<script>` to your page. There will be a global variable `matrixcs`
|
||||
|
||||
+5
-4
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "26.1.0",
|
||||
"version": "26.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -55,10 +55,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.1",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
@@ -101,7 +102,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
|
||||
@@ -19,7 +19,8 @@ import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||
import { createClient, MatrixClient, IAuthDict, UIAuthCallback } from "../../../src";
|
||||
import { createClient, IAuthDict, MatrixClient } from "../../../src";
|
||||
import { mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -62,45 +63,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
});
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
*
|
||||
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cross-signing keys, publish the keys
|
||||
* Mock and bootstrap all the required steps
|
||||
* Create cross-signing keys and publish the keys
|
||||
*
|
||||
* @param authDict - The parameters to as the `auth` dict in the key upload request.
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
|
||||
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
|
||||
await makeRequest(authDict);
|
||||
};
|
||||
|
||||
// now bootstrap cross signing, and check it resolves successfully
|
||||
await aliceClient.getCrypto()?.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: uiaCallback,
|
||||
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,9 @@ 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 } from "../../test-utils/mockEndpoints";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { CryptoCallbacks } from "../../../src/crypto-api";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -530,32 +531,58 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
/**
|
||||
* Create the {@link CryptoCallbacks}
|
||||
*/
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||
let cachedKey: { keyId: string; key: Uint8Array };
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: AddSecretStorageKeyOpts, key: Uint8Array) => {
|
||||
cachedKey = {
|
||||
keyId,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: "@alice:localhost",
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
});
|
||||
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
||||
|
||||
/* set up listeners for /keys/upload and /sync */
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
return {
|
||||
cacheSecretStorageKey,
|
||||
getSecretStorageKey,
|
||||
};
|
||||
}
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
});
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: "@alice:localhost",
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
});
|
||||
|
||||
/* set up listeners for /keys/upload and /sync */
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
@@ -2183,16 +2210,16 @@ 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`
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/r0/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
|
||||
*/
|
||||
function awaitKeyStoredInAccountData(): Promise<string> {
|
||||
function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
// 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",
|
||||
"express:/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
@@ -2207,6 +2234,25 @@ 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}`
|
||||
* Resolved when the cross signing key is uploaded
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
function awaitCrossSigningKeyUpload(key: string): Promise<Record<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}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -2245,12 +2291,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await startClientAndAwaitFirstSync();
|
||||
});
|
||||
|
||||
newBackendOnly("should do no nothing if createSecretStorageKey is not set", async () => {
|
||||
await aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true });
|
||||
|
||||
// No key was created
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
newBackendOnly(
|
||||
"should throw an error if we are unable to create a key because createSecretStorageKey is not set",
|
||||
async () => {
|
||||
await expect(
|
||||
aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true }),
|
||||
).rejects.toThrow("unable to create a new secret storage key, createSecretStorageKey is not set");
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly("should create a new key", async () => {
|
||||
const bootstrapPromise = aliceClient
|
||||
@@ -2258,7 +2306,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitKeyStoredInAccountData();
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
@@ -2279,7 +2327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitKeyStoredInAccountData();
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
@@ -2303,7 +2351,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
let secretStorageKey = await awaitKeyStoredInAccountData();
|
||||
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
@@ -2317,7 +2365,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
secretStorageKey = await awaitKeyStoredInAccountData();
|
||||
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
@@ -2329,5 +2377,38 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly("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({});
|
||||
|
||||
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
|
||||
const bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// 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
|
||||
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
|
||||
awaitCrossSigningKeyUpload("master"),
|
||||
awaitCrossSigningKeyUpload("user_signing"),
|
||||
awaitCrossSigningKeyUpload("self_signing"),
|
||||
]);
|
||||
|
||||
// Finally, wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// Expect the cross signing master key to be uploaded and to be encrypted with `secretStorageKey`
|
||||
expect(masterKey[secretStorageKey]).toBeDefined();
|
||||
expect(userSigningKey[secretStorageKey]).toBeDefined();
|
||||
expect(selfSigningKey[secretStorageKey]).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||
import anotherjson from "another-json";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
|
||||
import {
|
||||
canAcceptVerificationRequest,
|
||||
ShowQrCodeCallbacks,
|
||||
@@ -40,45 +45,41 @@ import {
|
||||
TEST_USER_ID,
|
||||
} from "../../test-utils/test-data";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
jest.useFakeTimers();
|
||||
|
||||
let previousCrypto: Crypto | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
// Stub out global.crypto
|
||||
previousCrypto = global["crypto"];
|
||||
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: {
|
||||
getRandomValues: function <T extends Uint8Array>(array: T): T {
|
||||
array.fill(0x12);
|
||||
return array;
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
// restore the original global.crypto
|
||||
afterAll(() => {
|
||||
if (previousCrypto === undefined) {
|
||||
// @ts-ignore deleting a non-optional property. It *is* optional really.
|
||||
delete global.crypto;
|
||||
} else {
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: previousCrypto,
|
||||
});
|
||||
}
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
beforeAll(async () => {
|
||||
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-js");
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 10000);
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
|
||||
const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
|
||||
/**
|
||||
* Integration tests for verification functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
// we test with both crypto stacks...
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
@@ -87,187 +88,310 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
/** the client under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "device_under_test",
|
||||
});
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
aliceClient.startClient();
|
||||
});
|
||||
|
||||
oldBackendOnly("Outgoing verification: can verify another device via SAS", async () => {
|
||||
// expect requests to download our own keys
|
||||
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||
device_keys: {
|
||||
[TEST_USER_ID]: {
|
||||
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
describe("Outgoing verification requests for another device", () => {
|
||||
beforeEach(async () => {
|
||||
// pretend that we have another device, which we will verify
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
});
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
let [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId;
|
||||
expect(transactionId).toBeDefined();
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||
const TEST_METHODS = ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
|
||||
it.each([undefined, TEST_METHODS])("can verify via SAS (supported methods=%s)", async (methods) => {
|
||||
aliceClient = await startTestClient({ verificationMethods: methods });
|
||||
await waitForDeviceList();
|
||||
|
||||
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.sas.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
// initially there should be no verifications in progress
|
||||
{
|
||||
const requests = aliceClient.getCrypto()!.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
expect(requests.length).toEqual(0);
|
||||
}
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready...
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.ready",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
let [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
expect(transactionId).toBeDefined();
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
expect(request.isSelfVerification).toBe(true);
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(false); // no reply yet
|
||||
expect(request.chosenMethod).toBe(null); // nothing chosen yet
|
||||
expect(request.initiatedByMe).toBe(true);
|
||||
expect(request.otherUserId).toEqual(TEST_USER_ID);
|
||||
expect(request.pending).toBe(true);
|
||||
// we're using fake timers, so the timeout should have exactly 10 minutes left still.
|
||||
expect(request.timeout).toEqual(600_000);
|
||||
|
||||
// and now the request should be visible via `getVerificationRequestsToDeviceInProgress`
|
||||
{
|
||||
const requests = aliceClient.getCrypto()!.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(requests[0].transactionId).toEqual(transactionId);
|
||||
}
|
||||
|
||||
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
if (methods !== undefined) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
|
||||
}
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready...
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);
|
||||
|
||||
// ... and picks a method with m.key.verification.start
|
||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||
|
||||
// as soon as the Changed event arrives, `verifier` should be defined
|
||||
const verifier = await new Promise<Verifier>((resolve) => {
|
||||
function onChange() {
|
||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
|
||||
resolve(verifier);
|
||||
}
|
||||
request.once(VerificationRequestEvent.Change, onChange);
|
||||
});
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await sendToDevicePromise;
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519-hkdf-sha256");
|
||||
expect(toDeviceMessage.short_authentication_string).toEqual(["decimal", "emoji"]);
|
||||
const macMethod = toDeviceMessage.message_authentication_code;
|
||||
expect(macMethod).toEqual("hkdf-hmac-sha256.v2");
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||
|
||||
// ... and the client is notified to show the emoji
|
||||
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||
});
|
||||
|
||||
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// user confirms that the emoji match, and alice sends a 'mac'
|
||||
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
returnToDeviceMessageFromSync(
|
||||
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||
);
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// the dummy device also confirms done-ness
|
||||
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
expect(request.pending).toBe(false);
|
||||
|
||||
// at this point, cancelling should do nothing.
|
||||
await request.cancel();
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
|
||||
// we're done with the temporary keypair
|
||||
olmSAS.free();
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);
|
||||
|
||||
// ... and picks a method with m.key.verification.start
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
it("can initiate SAS verification ourselves", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
// Alice sends a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
// And now Alice starts a SAS verification
|
||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
await request.startVerification("m.sas.v1");
|
||||
let requestBody = await sendToDevicePromise;
|
||||
|
||||
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage).toEqual({
|
||||
from_device: aliceClient.deviceId,
|
||||
method: "m.sas.v1",
|
||||
transaction_id: transactionId,
|
||||
hashes: ["sha256"],
|
||||
key_agreement_protocols: ["curve25519"],
|
||||
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
|
||||
short_authentication_string: ["emoji"],
|
||||
},
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||
|
||||
// there should now be a verifier
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519");
|
||||
expect(toDeviceMessage.short_authentication_string).toEqual(["emoji"]);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.key",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
key: olmSAS.get_pubkey(),
|
||||
},
|
||||
});
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||
|
||||
// ... and the client is notified to show the emoji
|
||||
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||
});
|
||||
|
||||
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// user confirms that the emoji match, and alice sends a 'mac'
|
||||
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`;
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.mac",
|
||||
content: {
|
||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||
transaction_id: transactionId,
|
||||
mac: {
|
||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||
olmSAS,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
|
||||
// we're done with the temporary keypair
|
||||
olmSAS.free();
|
||||
});
|
||||
|
||||
oldBackendOnly(
|
||||
"Outgoing verification: can verify another device via QR code with an untrusted cross-signing key",
|
||||
async () => {
|
||||
// expect requests to download our own keys
|
||||
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||
device_keys: {
|
||||
[TEST_USER_ID]: {
|
||||
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
...SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
key_agreement_protocols: expect.arrayContaining(["curve25519-hkdf-sha256"]),
|
||||
message_authentication_codes: expect.arrayContaining(["hkdf-hmac-sha256.v2"]),
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
});
|
||||
|
||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||
|
||||
// There should now be a `verifier`
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
const verificationPromise = verifier.verify();
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
returnToDeviceMessageFromSync(buildSasAcceptMessage(transactionId, commitmentStr));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await sendToDevicePromise;
|
||||
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||
|
||||
// ... and the dummy device also sends a 'key'
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// ... and the client is notified to show the emoji
|
||||
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||
});
|
||||
|
||||
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// user confirms that the emoji match, and alice sends a 'mac'
|
||||
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
returnToDeviceMessageFromSync(
|
||||
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||
);
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// the dummy device also confirms done-ness
|
||||
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
|
||||
// we're done with the temporary keypair
|
||||
olmSAS.free();
|
||||
});
|
||||
|
||||
it("Can make a verification request to *all* devices", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
// we need an existing cross-signing key for this
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
await waitForDeviceList();
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestOwnUserVerification(),
|
||||
]);
|
||||
|
||||
const transactionId = request.transactionId;
|
||||
expect(transactionId).toBeDefined();
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
|
||||
// and now the request should be visible via `getVerificationRequestsToDeviceInProgress`
|
||||
{
|
||||
const requests = aliceClient.getCrypto()!.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(requests[0].transactionId).toEqual(transactionId);
|
||||
}
|
||||
|
||||
// legacy crypto picks devices individually; rust crypto uses a broadcast message
|
||||
const toDeviceMessage =
|
||||
requestBody.messages[TEST_USER_ID]["*"] ?? requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
});
|
||||
|
||||
oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
|
||||
//
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
await waitForDeviceList();
|
||||
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
@@ -275,24 +399,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId;
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1");
|
||||
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
|
||||
expect(toDeviceMessage.methods).toContain("m.reciprocate.v1");
|
||||
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.ready",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.qr_code.scan.v1"],
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
});
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.scan.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
|
||||
@@ -351,52 +468,175 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||
},
|
||||
);
|
||||
|
||||
oldBackendOnly("Incoming verification: can accept", async () => {
|
||||
// expect requests to download our own keys
|
||||
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||
device_keys: {
|
||||
[TEST_USER_ID]: {
|
||||
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TRANSACTION_ID = "abcd";
|
||||
|
||||
// Initiate the request by sending a to-device message
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: TRANSACTION_ID,
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
});
|
||||
const request: VerificationRequest = await emitPromise(aliceClient, CryptoEvent.VerificationRequest);
|
||||
expect(request.transactionId).toEqual(TRANSACTION_ID);
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
expect(canAcceptVerificationRequest(request)).toBe(true);
|
||||
|
||||
// Alice accepts, by sending a to-device message
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
|
||||
const acceptPromise = request.accept();
|
||||
expect(canAcceptVerificationRequest(request)).toBe(false);
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
await acceptPromise;
|
||||
const requestBody = await sendToDevicePromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.sas.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
|
||||
});
|
||||
|
||||
describe("cancellation", () => {
|
||||
beforeEach(async () => {
|
||||
// pretend that we have another device, which we will start verifying
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
});
|
||||
|
||||
it("can cancel during the Ready phase", async () => {
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready...
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
|
||||
// now alice changes her mind
|
||||
const [requestBody] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.cancel"),
|
||||
request.cancel(),
|
||||
]);
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
expect(toDeviceMessage.code).toEqual("m.user");
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
});
|
||||
|
||||
it("can cancel during the SAS phase", async () => {
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready...
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
|
||||
// ... and picks a method with m.key.verification.start
|
||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||
|
||||
// there should now be a verifier...
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.hasBeenCancelled).toBe(false);
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
await sendToDevicePromise;
|
||||
|
||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
||||
verifier.cancel(new Error("blah"));
|
||||
await requestPromise;
|
||||
|
||||
// ... which should cancel the verifier
|
||||
await expect(verificationPromise).rejects.toThrow();
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
expect(verifier.hasBeenCancelled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Incoming verification from another device", () => {
|
||||
beforeEach(async () => {
|
||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
await waitForDeviceList();
|
||||
});
|
||||
|
||||
it("Incoming verification: can accept", async () => {
|
||||
const TRANSACTION_ID = "abcd";
|
||||
|
||||
// Initiate the request by sending a to-device message
|
||||
returnToDeviceMessageFromSync(buildRequestMessage(TRANSACTION_ID));
|
||||
const request: VerificationRequest = await emitPromise(
|
||||
aliceClient,
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
);
|
||||
expect(request.transactionId).toEqual(TRANSACTION_ID);
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
expect(request.initiatedByMe).toBe(false);
|
||||
expect(request.otherUserId).toEqual(TEST_USER_ID);
|
||||
expect(request.chosenMethod).toBe(null); // nothing chosen yet
|
||||
expect(canAcceptVerificationRequest(request)).toBe(true);
|
||||
expect(request.pending).toBe(true);
|
||||
|
||||
// Alice accepts, by sending a to-device message
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
|
||||
const acceptPromise = request.accept();
|
||||
expect(canAcceptVerificationRequest(request)).toBe(false);
|
||||
expect(request.phase).toEqual(VerificationPhase.Requested);
|
||||
await acceptPromise;
|
||||
const requestBody = await sendToDevicePromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.sas.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
|
||||
});
|
||||
|
||||
it("Incoming verification: can refuse", async () => {
|
||||
const TRANSACTION_ID = "abcd";
|
||||
|
||||
// Initiate the request by sending a to-device message
|
||||
returnToDeviceMessageFromSync(buildRequestMessage(TRANSACTION_ID));
|
||||
const request: VerificationRequest = await emitPromise(
|
||||
aliceClient,
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
);
|
||||
expect(request.transactionId).toEqual(TRANSACTION_ID);
|
||||
|
||||
// Alice declines, by sending a cancellation
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
||||
const cancelPromise = request.cancel();
|
||||
expect(canAcceptVerificationRequest(request)).toBe(false);
|
||||
expect(request.accepting).toBe(false);
|
||||
expect(request.declining).toBe(true);
|
||||
await cancelPromise;
|
||||
const requestBody = await sendToDevicePromise;
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
|
||||
});
|
||||
});
|
||||
|
||||
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
const client = createClient({
|
||||
baseUrl: TEST_HOMESERVER_URL,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "device_under_test",
|
||||
...opts,
|
||||
});
|
||||
await initCrypto(client);
|
||||
await client.startClient();
|
||||
return client;
|
||||
}
|
||||
|
||||
/** 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
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
|
||||
}
|
||||
|
||||
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||
ev.sender ??= TEST_USER_ID;
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||
@@ -443,6 +683,114 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
||||
return mac;
|
||||
}
|
||||
|
||||
/** Calculate the sha256 hash of a string, encoding as unpadded base64 */
|
||||
function sha256(commitmentStr: string): string {
|
||||
return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest());
|
||||
}
|
||||
|
||||
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
/** build an m.key.verification.request to-device message originating from the dummy device */
|
||||
function buildRequestMessage(transactionId: string): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: transactionId,
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.ready to-device message originating from the dummy device */
|
||||
function buildReadyMessage(transactionId: string, methods: string[]): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.ready",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: methods,
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */
|
||||
function buildSasStartMessage(transactionId: string): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
method: "m.sas.v1",
|
||||
transaction_id: transactionId,
|
||||
hashes: ["sha256"],
|
||||
key_agreement_protocols: ["curve25519-hkdf-sha256"],
|
||||
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
|
||||
// we have to include "decimal" per the spec.
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.accept to-device message suitable for the SAS flow */
|
||||
function buildSasAcceptMessage(transactionId: string, commitmentStr: string) {
|
||||
return {
|
||||
type: "m.key.verification.accept",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
commitment: sha256(commitmentStr),
|
||||
hash: "sha256",
|
||||
key_agreement_protocol: "curve25519-hkdf-sha256",
|
||||
short_authentication_string: ["decimal", "emoji"],
|
||||
message_authentication_code: "hkdf-hmac-sha256.v2",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.key to-device message suitable for the SAS flow */
|
||||
function buildSasKeyMessage(transactionId: string, key: string): { type: string; content: object } {
|
||||
return {
|
||||
type: "m.key.verification.key",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
key: key,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.mac to-device message suitable for the SAS flow, originating from the dummy device */
|
||||
function buildSasMacMessage(
|
||||
transactionId: string,
|
||||
olmSAS: Olm.SAS,
|
||||
recipientUserId: string,
|
||||
recipientDeviceId: string,
|
||||
): { type: string; content: object } {
|
||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${recipientUserId}${recipientDeviceId}${transactionId}`;
|
||||
|
||||
return {
|
||||
type: "m.key.verification.mac",
|
||||
content: {
|
||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||
transaction_id: transactionId,
|
||||
mac: {
|
||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||
olmSAS,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** build an m.key.verification.done to-device message */
|
||||
function buildDoneMessage(transactionId: string) {
|
||||
return {
|
||||
type: "m.key.verification.done",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -598,12 +598,6 @@ describe("MatrixClient event timelines", function () {
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -634,12 +628,6 @@ describe("MatrixClient event timelines", function () {
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
await flushHttp(emitPromise(thread, ThreadEvent.Update));
|
||||
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
|
||||
@@ -1510,7 +1498,8 @@ describe("MatrixClient event timelines", function () {
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
// this has to come after THREAD_REPLY which hasn't been instantiated by us
|
||||
THREAD_REPLY2.localTimestamp += 10000000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
@@ -1570,9 +1559,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -1699,13 +1685,11 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
|
||||
// Test thread order is unchanged
|
||||
expect(timeline!.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
@@ -2021,25 +2005,6 @@ describe("MatrixClient event timelines", function () {
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -2047,71 +2012,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
from: "start_token",
|
||||
}),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [],
|
||||
};
|
||||
});
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Forward, from: "end_token" }),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
|
||||
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||
const timeline = await client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
|
||||
@@ -145,6 +145,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* If the device keys have already been uploaded, return them. Else return null.
|
||||
*/
|
||||
public getUploadedDeviceKeys(): IDeviceKeys | null {
|
||||
return this.deviceKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { IDownloadKeyResult } from "../../src";
|
||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||
*/
|
||||
export class E2EKeyResponder {
|
||||
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
|
||||
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
|
||||
private masterKeysByUser: Record<string, any> = {};
|
||||
private selfSigningKeysByUser: Record<string, any> = {};
|
||||
private userSigningKeysByUser: Record<string, any> = {};
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/query` requests for the given homeserverUrl.
|
||||
* Only /query requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
private onKeyQueryRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const usersToReturn = Object.keys(content["device_keys"]);
|
||||
const response = {
|
||||
device_keys: {} as { [userId: string]: any },
|
||||
master_keys: {} as { [userId: string]: any },
|
||||
self_signing_keys: {} as { [userId: string]: any },
|
||||
user_signing_keys: {} as { [userId: string]: any },
|
||||
failures: {} as { [serverName: string]: any },
|
||||
};
|
||||
for (const user of usersToReturn) {
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
||||
}
|
||||
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
|
||||
if (e2eKeyReceiver !== undefined) {
|
||||
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
|
||||
if (deviceKeys !== null) {
|
||||
response.device_keys[user] ??= {};
|
||||
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
||||
response.master_keys[user] = this.masterKeysByUser[user];
|
||||
}
|
||||
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
|
||||
}
|
||||
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
|
||||
*
|
||||
* @param userId - user the keys belong to
|
||||
* @param deviceId - device the keys belong to
|
||||
* @param keys - device keys for this device.
|
||||
*/
|
||||
public addDeviceKeys(userId: string, deviceId: string, keys: IDeviceKeys) {
|
||||
this.deviceKeysByUserByDevice.getOrCreate(userId).set(deviceId, keys);
|
||||
}
|
||||
|
||||
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
|
||||
*
|
||||
* @param data cross-signing data
|
||||
*/
|
||||
public addCrossSigningData(
|
||||
data: Pick<IDownloadKeyResult, "master_keys" | "self_signing_keys" | "user_signing_keys">,
|
||||
) {
|
||||
Object.assign(this.masterKeysByUser, data.master_keys);
|
||||
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
|
||||
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an E2EKeyReceiver to poll for uploaded keys
|
||||
*
|
||||
* Any keys which have been uploaded to the given `E2EKeyReceiver` at the time of the `/keys/query` request will
|
||||
* be added to the response.
|
||||
*
|
||||
* @param e2eKeyReceiver
|
||||
*/
|
||||
public addKeyReceiver(userId: string, e2eKeyReceiver: E2EKeyReceiver) {
|
||||
this.e2eKeyReceiversByUser.set(userId, e2eKeyReceiver);
|
||||
}
|
||||
}
|
||||
@@ -28,3 +28,28 @@ export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
filter_id: "fid",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
*
|
||||
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
export function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export const mkThread = ({
|
||||
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, [rootEvent, ...events], true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcDiscoveryError } from "../../src/oidc/validate";
|
||||
import { OidcError } from "../../src/oidc/error";
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
@@ -400,7 +400,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -441,7 +441,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -485,7 +485,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "FAIL_ERROR",
|
||||
error: OidcDiscoveryError.Misconfigured,
|
||||
error: OidcError.Misconfigured,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -719,7 +719,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -775,7 +775,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
DuplicateStrategy,
|
||||
@@ -160,6 +162,33 @@ describe("EventTimelineSet", () => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
||||
// If threads are disabled all events go into the main timeline
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents()).toHaveLength(1);
|
||||
const [event] = liveTimeline.getEvents();
|
||||
const reactions = eventTimelineSet.relations!.getChildEventsForEvent(
|
||||
event.getId()!,
|
||||
"m.annotation",
|
||||
"m.reaction",
|
||||
)!;
|
||||
const relations = reactions.getRelations();
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0].getId()).toBe(reactionEvent.getId());
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventToTimeline (thread timeline)", () => {
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("InteractiveAuth", () => {
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
errcode: "MockError0",
|
||||
params: {
|
||||
[AuthType.Password]: { param: "aa" },
|
||||
},
|
||||
|
||||
@@ -308,4 +308,25 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore thread relation on state events", async () => {
|
||||
const stateEvent = new MatrixEvent({
|
||||
event_id: "$event_id",
|
||||
type: "some_state_event",
|
||||
content: {
|
||||
"foo": "bar",
|
||||
"m.relates_to": {
|
||||
"event_id": "$thread_id",
|
||||
"m.in_reply_to": {
|
||||
event_id: "$thread_id",
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
state_key: "",
|
||||
});
|
||||
|
||||
expect(stateEvent.isState()).toBeTruthy();
|
||||
expect(stateEvent.threadRootId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { mocked } from "jest-mock";
|
||||
|
||||
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
|
||||
import { Room, RoomEvent } from "../../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent, FeatureSupport } from "../../../src/models/thread";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
|
||||
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
@@ -43,6 +43,7 @@ describe("Thread", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -300,6 +301,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -354,6 +356,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -405,6 +408,7 @@ describe("Thread", () => {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -699,11 +703,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
|
||||
root.setThreadId(root.getId());
|
||||
await room.addLiveEvents([root]);
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
|
||||
|
||||
return thread;
|
||||
return room.createThread(root.getId()!, root, [], false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { Method } from "../../../src";
|
||||
import * as crypto from "../../../src/crypto/crypto";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
completeAuthorizationCodeGrant,
|
||||
generateAuthorizationParams,
|
||||
generateAuthorizationUrl,
|
||||
} from "../../../src/oidc/authorize";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
// save for resetting mocks
|
||||
const realSubtleCrypto = crypto.subtleCrypto;
|
||||
|
||||
describe("oidc authorization", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const authorizationEndpoint = "https://auth.com/authorization";
|
||||
const tokenEndpoint = "https://auth.com/token";
|
||||
const delegatedAuthConfig = {
|
||||
issuer,
|
||||
registrationEndpoint: issuer + "registration",
|
||||
authorizationEndpoint: issuer + "auth",
|
||||
tokenEndpoint,
|
||||
};
|
||||
const clientId = "xyz789";
|
||||
const baseUrl = "https://test.com";
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(logger, "warn");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore reset any ugly mocking we did
|
||||
crypto.subtleCrypto = realSubtleCrypto;
|
||||
});
|
||||
|
||||
it("should generate authorization params", () => {
|
||||
const result = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
|
||||
expect(result.redirectUri).toEqual(baseUrl);
|
||||
|
||||
// random strings
|
||||
expect(result.state.length).toEqual(8);
|
||||
expect(result.nonce.length).toEqual(8);
|
||||
expect(result.codeVerifier.length).toEqual(64);
|
||||
|
||||
const expectedScope =
|
||||
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:";
|
||||
expect(result.scope.startsWith(expectedScope)).toBeTruthy();
|
||||
// deviceId of 10 characters is appended to the device scope
|
||||
expect(result.scope.length).toEqual(expectedScope.length + 10);
|
||||
});
|
||||
|
||||
describe("generateAuthorizationUrl()", () => {
|
||||
it("should generate url with correct parameters", async () => {
|
||||
// test the no crypto case here
|
||||
// @ts-ignore mocking
|
||||
crypto.subtleCrypto = undefined;
|
||||
|
||||
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
const authUrl = new URL(
|
||||
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
|
||||
);
|
||||
|
||||
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
|
||||
expect(authUrl.searchParams.get("response_type")).toEqual("code");
|
||||
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
|
||||
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
|
||||
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
|
||||
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
|
||||
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
|
||||
|
||||
// crypto not available, plain text code_challenge is used
|
||||
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"A secure context is required to generate code challenge. Using plain text code challenge",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a s256 code challenge when crypto is available", async () => {
|
||||
jest.spyOn(crypto.subtleCrypto, "digest");
|
||||
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
const authUrl = new URL(
|
||||
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
|
||||
);
|
||||
|
||||
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
||||
expect(crypto.subtleCrypto.digest).toHaveBeenCalledWith("SHA-256", expect.any(Object));
|
||||
|
||||
// didn't use plain text code challenge
|
||||
expect(authorizationParams.codeVerifier).not.toEqual(codeChallenge);
|
||||
expect(codeChallenge).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("completeAuthorizationCodeGrant", () => {
|
||||
const codeVerifier = "abc123";
|
||||
const nonce = "test-nonce";
|
||||
const redirectUri = baseUrl;
|
||||
const code = "auth_code_xyz";
|
||||
const validBearerTokenResponse = {
|
||||
token_type: "Bearer",
|
||||
access_token: "test_access_token",
|
||||
refresh_token: "test_refresh_token",
|
||||
id_token: "valid.id.token",
|
||||
expires_in: 12345,
|
||||
};
|
||||
|
||||
const validDecodedIdToken = {
|
||||
// nonce matches
|
||||
nonce,
|
||||
// not expired
|
||||
exp: Date.now() / 1000 + 100000,
|
||||
// audience is this client
|
||||
aud: clientId,
|
||||
// issuer matches
|
||||
iss: delegatedAuthConfig.issuer,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.resetBehavior();
|
||||
|
||||
fetchMock.post(tokenEndpoint, {
|
||||
status: 200,
|
||||
body: JSON.stringify(validBearerTokenResponse),
|
||||
});
|
||||
|
||||
mocked(jwtDecode).mockReturnValue(validDecodedIdToken);
|
||||
});
|
||||
|
||||
it("should make correct request to the token endpoint", async () => {
|
||||
await completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(tokenEndpoint, {
|
||||
method: Method.Post,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: `grant_type=authorization_code&client_id=${clientId}&code_verifier=${codeVerifier}&redirect_uri=https%3A%2F%2Ftest.com&code=${code}`,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return with valid bearer token", async () => {
|
||||
const result = await completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
expect(result).toEqual(validBearerTokenResponse);
|
||||
});
|
||||
|
||||
it("should return with valid bearer token where token_type is lowercase", async () => {
|
||||
const tokenResponse = {
|
||||
...validBearerTokenResponse,
|
||||
token_type: "bearer",
|
||||
};
|
||||
fetchMock.post(
|
||||
tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
body: JSON.stringify(tokenResponse),
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const result = await completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
});
|
||||
|
||||
// results in token that uses 'Bearer' token type
|
||||
expect(result).toEqual(validBearerTokenResponse);
|
||||
expect(result.token_type).toEqual("Bearer");
|
||||
});
|
||||
|
||||
it("should throw with code exchange failed error when request fails", async () => {
|
||||
fetchMock.post(
|
||||
tokenEndpoint,
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.CodeExchangeFailed));
|
||||
});
|
||||
|
||||
it("should throw invalid token error when token is invalid", async () => {
|
||||
const invalidBearerTokenResponse = {
|
||||
...validBearerTokenResponse,
|
||||
access_token: null,
|
||||
};
|
||||
fetchMock.post(
|
||||
tokenEndpoint,
|
||||
{ status: 200, body: JSON.stringify(invalidBearerTokenResponse) },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.InvalidBearerTokenResponse));
|
||||
});
|
||||
|
||||
it("should throw invalid id token error when id_token is invalid", async () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
// invalid audience
|
||||
aud: "something-else",
|
||||
});
|
||||
await expect(() =>
|
||||
completeAuthorizationCodeGrant(code, {
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}),
|
||||
).rejects.toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
import { registerOidcClient } from "../../../src/oidc/register";
|
||||
|
||||
describe("registerOidcClient()", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const registrationEndpoint = "https://auth.com/register";
|
||||
const clientName = "Element";
|
||||
const baseUrl = "https://just.testing";
|
||||
const dynamicClientId = "xyz789";
|
||||
|
||||
const delegatedAuthConfig = {
|
||||
issuer,
|
||||
registrationEndpoint,
|
||||
authorizationEndpoint: issuer + "auth",
|
||||
tokenEndpoint: issuer + "token",
|
||||
};
|
||||
beforeEach(() => {
|
||||
fetchMockJest.mockClear();
|
||||
fetchMockJest.resetBehavior();
|
||||
});
|
||||
|
||||
it("should make correct request to register client", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||
});
|
||||
expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
client_name: clientName,
|
||||
client_uri: baseUrl,
|
||||
response_types: ["code"],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
redirect_uris: [baseUrl],
|
||||
id_token_signed_response_alg: "RS256",
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw when registration request fails", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
status: 500,
|
||||
});
|
||||
expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationFailed,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when registration response is invalid", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
status: 200,
|
||||
// no clientId in response
|
||||
body: "{}",
|
||||
});
|
||||
expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationInvalid,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { M_AUTHENTICATION } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
OidcDiscoveryError,
|
||||
validateIdToken,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "../../../src/oidc/validate";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
describe("validateWellKnownAuthentication()", () => {
|
||||
const baseWk = {
|
||||
@@ -29,7 +35,7 @@ describe("validateWellKnownAuthentication()", () => {
|
||||
},
|
||||
};
|
||||
it("should throw not supported error when wellKnown has no m.authentication section", () => {
|
||||
expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcDiscoveryError.NotSupported);
|
||||
expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcError.NotSupported);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication issuer is not a string", () => {
|
||||
@@ -39,7 +45,7 @@ describe("validateWellKnownAuthentication()", () => {
|
||||
issuer: { url: "test.com" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is not a string", () => {
|
||||
@@ -50,7 +56,7 @@ describe("validateWellKnownAuthentication()", () => {
|
||||
account: { url: "test" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is false", () => {
|
||||
@@ -61,7 +67,7 @@ describe("validateWellKnownAuthentication()", () => {
|
||||
account: false,
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should return valid config when wk uses stable m.authentication", () => {
|
||||
@@ -137,7 +143,7 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
it("should throw OP support error when wellKnown is not an object", () => {
|
||||
expect(() => {
|
||||
validateOIDCIssuerWellKnown([]);
|
||||
}).toThrow(OidcDiscoveryError.OpSupport);
|
||||
}).toThrow(OidcError.OpSupport);
|
||||
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
||||
});
|
||||
|
||||
@@ -148,7 +154,7 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
authorization_endpoint: undefined,
|
||||
response_types_supported: [],
|
||||
});
|
||||
}).toThrow(OidcDiscoveryError.OpSupport);
|
||||
}).toThrow(OidcError.OpSupport);
|
||||
expect(logger.error).toHaveBeenCalledWith("OIDC issuer configuration: authorization_endpoint is invalid");
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"OIDC issuer configuration: response_types_supported is invalid. code is required.",
|
||||
@@ -194,6 +200,99 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
...validWk,
|
||||
[key]: value,
|
||||
};
|
||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcDiscoveryError.OpSupport);
|
||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateIdToken()", () => {
|
||||
const nonce = "test-nonce";
|
||||
const issuer = "https://auth.org/issuer";
|
||||
const clientId = "test-client-id";
|
||||
const idToken = "test-id-token";
|
||||
|
||||
const validDecodedIdToken = {
|
||||
// nonce matches
|
||||
nonce,
|
||||
// not expired
|
||||
exp: Date.now() / 1000 + 5555,
|
||||
// audience is this client
|
||||
aud: clientId,
|
||||
// issuer matches
|
||||
iss: issuer,
|
||||
};
|
||||
beforeEach(() => {
|
||||
mocked(jwtDecode).mockClear().mockReturnValue(validDecodedIdToken);
|
||||
|
||||
jest.spyOn(logger, "error").mockClear();
|
||||
});
|
||||
|
||||
it("should throw when idToken is falsy", () => {
|
||||
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
|
||||
it("should throw when idToken cannot be decoded", () => {
|
||||
mocked(jwtDecode).mockImplementation(() => {
|
||||
throw new Error("oh no!");
|
||||
});
|
||||
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
});
|
||||
|
||||
it("should throw when issuer does not match", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
iss: "https://badissuer.com",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid issuer"));
|
||||
});
|
||||
|
||||
it("should throw when audience does not include clientId", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
aud: "qwerty,uiop,asdf",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
||||
});
|
||||
|
||||
it("should throw when audience includes clientId and other audiences", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
aud: `${clientId},uiop,asdf`,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
||||
});
|
||||
|
||||
it("should throw when nonce does not match", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
nonce: "something else",
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid nonce"));
|
||||
});
|
||||
|
||||
it("should throw when token does not have an expiry", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
exp: undefined,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
||||
});
|
||||
|
||||
it("should throw when token is expired", () => {
|
||||
mocked(jwtDecode).mockReturnValue({
|
||||
...validDecodedIdToken,
|
||||
// expired in the past
|
||||
exp: Date.now() / 1000 - 777,
|
||||
});
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
||||
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
||||
});
|
||||
|
||||
it("should not throw for a valid id token", () => {
|
||||
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2787,10 +2787,10 @@ describe("Room", function () {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
await room.addLiveEvents([threadRoot, threadResponse1]);
|
||||
const thread: Thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread.initialEventsFetched).toBeTruthy();
|
||||
await room.addLiveEvents([threadResponse2]);
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
@@ -24,6 +22,7 @@ import fetchMock from "fetch-mock-jest";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import {
|
||||
CryptoEvent,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
@@ -37,16 +36,9 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
import { CryptoCallbacks, ImportRoomKeysOpts } from "../../../src/crypto-api";
|
||||
import { CryptoCallbacks, ImportRoomKeysOpts, VerificationRequest } from "../../../src/crypto-api";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
@@ -54,9 +46,13 @@ describe("RustCrypto", () => {
|
||||
describe(".importRoomKeys and .exportRoomKeys", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
beforeEach(
|
||||
async () => {
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
it("should import and export keys", async () => {
|
||||
const someRoomKeys = [
|
||||
@@ -145,6 +141,27 @@ describe("RustCrypto", () => {
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
|
||||
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
||||
const toDeviceEvent = {
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: "testDeviceId",
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: "testTxn",
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
sender: "@user:id",
|
||||
};
|
||||
|
||||
const onEvent = jest.fn();
|
||||
rustCrypto.on(CryptoEvent.VerificationRequestReceived, onEvent);
|
||||
await rustCrypto.preprocessToDeviceMessages([toDeviceEvent]);
|
||||
expect(onEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [req]: [VerificationRequest] = onEvent.mock.lastCall;
|
||||
expect(req.transactionId).toEqual("testTxn");
|
||||
});
|
||||
});
|
||||
|
||||
it("getCrossSigningKeyId", async () => {
|
||||
@@ -376,7 +393,35 @@ describe("RustCrypto", () => {
|
||||
rustCrypto = await makeTestRustCrypto(undefined, testData.TEST_USER_ID);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns false initially", async () => {
|
||||
jest.useFakeTimers();
|
||||
const prom = rustCrypto.userHasCrossSigningKeys();
|
||||
// the getIdentity() request should wait for a /keys/query request to complete, but times out after 1500ms
|
||||
await jest.advanceTimersByTimeAsync(2000);
|
||||
await expect(prom).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if there is no cross-signing identity", async () => {
|
||||
// @ts-ignore private field
|
||||
const olmMachine = rustCrypto.olmMachine;
|
||||
|
||||
const outgoingRequests: OutgoingRequest[] = await olmMachine.outgoingRequests();
|
||||
// pick out the KeysQueryRequest, and respond to it with the device keys but *no* cross-signing keys.
|
||||
const req = outgoingRequests.find((r) => r instanceof KeysQueryRequest)!;
|
||||
await olmMachine.markRequestAsSent(
|
||||
req.id!,
|
||||
req.type,
|
||||
JSON.stringify({
|
||||
device_keys: {
|
||||
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
@@ -390,7 +435,12 @@ describe("RustCrypto", () => {
|
||||
await olmMachine.markRequestAsSent(
|
||||
req.id!,
|
||||
req.type,
|
||||
JSON.stringify(testData.SIGNED_CROSS_SIGNING_KEYS_DATA),
|
||||
JSON.stringify({
|
||||
device_keys: {
|
||||
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
|
||||
},
|
||||
...testData.SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
}),
|
||||
);
|
||||
|
||||
// ... and we should now have cross-signing keys.
|
||||
@@ -461,6 +511,15 @@ describe("RustCrypto", () => {
|
||||
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
|
||||
rustCrypto.stop();
|
||||
});
|
||||
|
||||
describe("requestDeviceVerification", () => {
|
||||
it("throws an error if the device is unknown", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(() => rustCrypto.requestDeviceVerification(TEST_USER, "unknown")).rejects.toThrow(
|
||||
"Not a known device",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a basic RustCrypto instance for testing
|
||||
@@ -474,5 +533,5 @@ async function makeTestRustCrypto(
|
||||
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
||||
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
|
||||
): Promise<RustCrypto> {
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import { RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("VerificationRequest", () => {
|
||||
describe("pending", () => {
|
||||
let request: RustVerificationRequest;
|
||||
let mockedInner: Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedInner = makeMockedInner();
|
||||
request = makeTestRequest(mockedInner);
|
||||
});
|
||||
|
||||
it("returns true for a created request", () => {
|
||||
expect(request.pending).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for passive requests", () => {
|
||||
mockedInner.isPassive.mockReturnValue(true);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for completed requests", () => {
|
||||
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Done);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for cancelled requests", () => {
|
||||
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Cancelled);
|
||||
expect(request.pending).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeout", () => {
|
||||
it("passes through the result", () => {
|
||||
const mockedInner = makeMockedInner();
|
||||
const request = makeTestRequest(mockedInner);
|
||||
mockedInner.timeRemainingMillis.mockReturnValue(10_000);
|
||||
expect(request.timeout).toEqual(10_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("startVerification", () => {
|
||||
let request: RustVerificationRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
request = makeTestRequest();
|
||||
});
|
||||
|
||||
it("does not permit methods other than SAS", async () => {
|
||||
await expect(request.startVerification("m.reciprocate.v1")).rejects.toThrow(
|
||||
"Unsupported verification method",
|
||||
);
|
||||
});
|
||||
|
||||
it("raises an error if starting verification does not produce a verifier", async () => {
|
||||
await expect(request.startVerification("m.sas.v1")).rejects.toThrow(
|
||||
"Still no verifier after startSas() call",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a RustVerificationRequest with default parameters */
|
||||
function makeTestRequest(
|
||||
inner?: RustSdkCryptoJs.VerificationRequest,
|
||||
outgoingRequestProcessor?: OutgoingRequestProcessor,
|
||||
): RustVerificationRequest {
|
||||
inner ??= makeMockedInner();
|
||||
outgoingRequestProcessor ??= {} as OutgoingRequestProcessor;
|
||||
return new RustVerificationRequest(inner, outgoingRequestProcessor, undefined);
|
||||
}
|
||||
|
||||
/** Mock up a rust-side VerificationRequest */
|
||||
function makeMockedInner(): Mocked<RustSdkCryptoJs.VerificationRequest> {
|
||||
return {
|
||||
registerChangesCallback: jest.fn(),
|
||||
startSas: jest.fn(),
|
||||
phase: jest.fn().mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Created),
|
||||
isPassive: jest.fn().mockReturnValue(false),
|
||||
timeRemainingMillis: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||
}
|
||||
+155
-13
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IClientWellKnown } from "../client";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
@@ -79,19 +80,6 @@ export interface IIdentityProvider {
|
||||
brand?: IdentityProviderBrand | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
export interface ILoginParams {
|
||||
identifier?: object;
|
||||
password?: string;
|
||||
token?: string;
|
||||
device_id?: string;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export enum SSOAction {
|
||||
/** The user intends to login to an existing account */
|
||||
LOGIN = "login",
|
||||
@@ -100,6 +88,160 @@ export enum SSOAction {
|
||||
REGISTER = "register",
|
||||
}
|
||||
|
||||
/**
|
||||
* A client can identify a user using their Matrix ID.
|
||||
* This can either be the fully qualified Matrix user ID, or just the localpart of the user ID.
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#matrix-user-id
|
||||
*/
|
||||
type UserLoginIdentifier = {
|
||||
type: "m.id.user";
|
||||
user: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client can identify a user using a 3PID associated with the user’s account on the homeserver,
|
||||
* where the 3PID was previously associated using the /account/3pid API.
|
||||
* See the 3PID Types Appendix for a list of Third-party ID media.
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#third-party-id
|
||||
*/
|
||||
type ThirdPartyLoginIdentifier = {
|
||||
type: "m.id.thirdparty";
|
||||
medium: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client can identify a user using a phone number associated with the user’s account,
|
||||
* where the phone number was previously associated using the /account/3pid API.
|
||||
* The phone number can be passed in as entered by the user; the homeserver will be responsible for canonicalising it.
|
||||
* If the client wishes to canonicalise the phone number,
|
||||
* then it can use the m.id.thirdparty identifier type with a medium of msisdn instead.
|
||||
*
|
||||
* The country is the two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone should be parsed as if it were dialled from.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#phone-number
|
||||
*/
|
||||
type PhoneLoginIdentifier = {
|
||||
type: "m.id.phone";
|
||||
country: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
type SpecUserIdentifier = UserLoginIdentifier | ThirdPartyLoginIdentifier | PhoneLoginIdentifier;
|
||||
|
||||
/**
|
||||
* User Identifiers usable for login & user-interactive authentication.
|
||||
*
|
||||
* Extensibly allows more than Matrix specified identifiers.
|
||||
*/
|
||||
export type UserIdentifier =
|
||||
| SpecUserIdentifier
|
||||
| { type: Exclude<string, SpecUserIdentifier["type"]>; [key: string]: any };
|
||||
|
||||
/**
|
||||
* Request body for POST /login request
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
/**
|
||||
* The login type being used.
|
||||
*/
|
||||
type: "m.login.password" | "m.login.token" | string;
|
||||
/**
|
||||
* Third-party identifier for the user.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
address?: string;
|
||||
/**
|
||||
* ID of the client device.
|
||||
* If this does not correspond to a known client device, a new device will be created.
|
||||
* The given device ID must not be the same as a cross-signing key ID.
|
||||
* The server will auto-generate a device_id if this is not specified.
|
||||
*/
|
||||
device_id?: string;
|
||||
/**
|
||||
* Identification information for a user
|
||||
*/
|
||||
identifier?: UserIdentifier;
|
||||
/**
|
||||
* A display name to assign to the newly-created device.
|
||||
* Ignored if device_id corresponds to a known device.
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* When logging in using a third-party identifier, the medium of the identifier.
|
||||
* Must be `email`.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
medium?: "email";
|
||||
/**
|
||||
* Required when type is `m.login.password`. The user’s password.
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* If true, the client supports refresh tokens.
|
||||
*/
|
||||
refresh_token?: boolean;
|
||||
/**
|
||||
* Required when type is `m.login.token`. Part of Token-based login.
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The fully qualified user ID or just local part of the user ID, to log in.
|
||||
* @deprecated in favour of identifier.
|
||||
*/
|
||||
user?: string;
|
||||
// Extensible
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Export for backwards compatibility
|
||||
export type ILoginParams = LoginRequest;
|
||||
|
||||
/**
|
||||
* Response body for POST /login request
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
/**
|
||||
* An access token for the account.
|
||||
* This access token can then be used to authorize other requests.
|
||||
*/
|
||||
access_token: string;
|
||||
/**
|
||||
* ID of the logged-in device.
|
||||
* Will be the same as the corresponding parameter in the request, if one was specified.
|
||||
*/
|
||||
device_id: string;
|
||||
/**
|
||||
* The fully-qualified Matrix ID for the account.
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* The lifetime of the access token, in milliseconds.
|
||||
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
|
||||
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
|
||||
* If not given, the client can assume that the access token will not expire.
|
||||
*/
|
||||
expires_in_ms?: number;
|
||||
/**
|
||||
* A refresh token for the account.
|
||||
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* Optional client configuration provided by the server.
|
||||
* If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within.
|
||||
* This object takes the same form as the one returned from .well-known autodiscovery.
|
||||
*/
|
||||
well_known?: IClientWellKnown;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
|
||||
* `m.login.token` issuance request.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthDict } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* The request body of a call to `POST /_matrix/client/v3/register`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||
*/
|
||||
export interface RegisterRequest {
|
||||
/**
|
||||
* Additional authentication information for the user-interactive authentication API.
|
||||
* Note that this information is not used to define how the registered user should be authenticated,
|
||||
* but is instead used to authenticate the register call itself.
|
||||
*/
|
||||
auth?: AuthDict;
|
||||
/**
|
||||
* The basis for the localpart of the desired Matrix ID.
|
||||
* If omitted, the homeserver MUST generate a Matrix ID local part.
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* The desired password for the account.
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* If true, the client supports refresh tokens.
|
||||
*/
|
||||
refresh_token?: boolean;
|
||||
/**
|
||||
* If true, an access_token and device_id should not be returned from this call, therefore preventing an automatic login.
|
||||
* Defaults to false.
|
||||
*/
|
||||
inhibit_login?: boolean;
|
||||
/**
|
||||
* A display name to assign to the newly-created device.
|
||||
* Ignored if device_id corresponds to a known device.
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
guest_access_token?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
x_show_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful call to `POST /_matrix/client/v3/register`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
||||
*/
|
||||
export interface RegisterResponse {
|
||||
/**
|
||||
* The fully-qualified Matrix user ID (MXID) that has been registered.
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* An access token for the account.
|
||||
* This access token can then be used to authorize other requests.
|
||||
* Required if the inhibit_login option is false.
|
||||
*/
|
||||
access_token?: string;
|
||||
/**
|
||||
* ID of the registered device.
|
||||
* Will be the same as the corresponding parameter in the request, if one was specified.
|
||||
* Required if the inhibit_login option is false.
|
||||
*/
|
||||
device_id?: string;
|
||||
/**
|
||||
* The lifetime of the access token, in milliseconds.
|
||||
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
|
||||
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
|
||||
* If not given, the client can assume that the access token will not expire.
|
||||
*
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
expires_in_ms?: number;
|
||||
/**
|
||||
* A refresh token for the account.
|
||||
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
|
||||
*
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
*
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
@@ -176,7 +176,8 @@ export interface IAddThreePidOnlyBody {
|
||||
export interface IBindThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
// Some older identity servers have no auth enabled
|
||||
id_access_token: string | null;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
|
||||
+6
-14
@@ -18,12 +18,8 @@ limitations under the License.
|
||||
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
import {
|
||||
OidcDiscoveryError,
|
||||
ValidatedIssuerConfig,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "./oidc/validate";
|
||||
import { ValidatedIssuerConfig, validateOIDCIssuerWellKnown, validateWellKnownAuthentication } from "./oidc/validate";
|
||||
import { OidcError } from "./oidc/error";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -297,7 +293,7 @@ export class AutoDiscovery {
|
||||
|
||||
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Failed to fetch issuer openid configuration");
|
||||
throw new Error(OidcDiscoveryError.General);
|
||||
throw new Error(OidcError.General);
|
||||
}
|
||||
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
|
||||
@@ -310,15 +306,11 @@ export class AutoDiscovery {
|
||||
};
|
||||
return delegatedAuthConfig;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcDiscoveryError;
|
||||
const errorType = Object.values(OidcDiscoveryError).includes(errorMessage)
|
||||
? errorMessage
|
||||
: OidcDiscoveryError.General;
|
||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcDiscoveryError.NotSupported
|
||||
? AutoDiscoveryAction.IGNORE
|
||||
: AutoDiscoveryAction.FAIL_ERROR;
|
||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
|
||||
+88
-61
@@ -101,7 +101,7 @@ import {
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
|
||||
import { IAuthData, IAuthDict } from "./interactive-auth";
|
||||
import { AuthDict } from "./interactive-auth";
|
||||
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
|
||||
import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./crypto/api";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
@@ -178,7 +178,14 @@ import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
|
||||
import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
import {
|
||||
LoginTokenPostResponse,
|
||||
ILoginFlowsResponse,
|
||||
IRefreshTokenResponse,
|
||||
SSOAction,
|
||||
LoginResponse,
|
||||
LoginRequest,
|
||||
} from "./@types/auth";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
@@ -209,6 +216,7 @@ import {
|
||||
ServerSideSecretStorage,
|
||||
ServerSideSecretStorageImpl,
|
||||
} from "./secret-storage";
|
||||
import { RegisterRequest, RegisterResponse } from "./@types/registration";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -336,6 +344,11 @@ export interface ICreateClientOpts {
|
||||
*/
|
||||
pickleKey?: string;
|
||||
|
||||
/**
|
||||
* Verification methods we should offer to the other side when performing an interactive verification.
|
||||
* If unset, we will offer all known methods. Currently these are: showing a QR code, scanning a QR code, and SAS
|
||||
* (aka "emojis").
|
||||
*/
|
||||
verificationMethods?: Array<VerificationMethod>;
|
||||
|
||||
/**
|
||||
@@ -367,6 +380,9 @@ export interface ICreateClientOpts {
|
||||
*/
|
||||
useE2eForGroupCall?: boolean;
|
||||
|
||||
/**
|
||||
* Crypto callbacks provided by the application
|
||||
*/
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
|
||||
/**
|
||||
@@ -709,18 +725,8 @@ interface IJoinedMembersResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRegisterRequestParams {
|
||||
auth?: IAuthDict;
|
||||
username?: string;
|
||||
password?: string;
|
||||
refresh_token?: boolean;
|
||||
guest_access_token?: string;
|
||||
x_show_msisdn?: boolean;
|
||||
bind_msisdn?: boolean;
|
||||
bind_email?: boolean;
|
||||
inhibit_login?: boolean;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
// Re-export for backwards compatibility
|
||||
export type IRegisterRequestParams = RegisterRequest;
|
||||
|
||||
export interface IPublicRoomsChunkRoom {
|
||||
room_id: string;
|
||||
@@ -863,6 +869,7 @@ export interface TimestampToEventResponse {
|
||||
interface IWhoamiResponse {
|
||||
user_id: string;
|
||||
device_id?: string;
|
||||
is_guest?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -915,6 +922,7 @@ type CryptoEvents =
|
||||
| CryptoEvent.RoomKeyRequest
|
||||
| CryptoEvent.RoomKeyRequestCancellation
|
||||
| CryptoEvent.VerificationRequest
|
||||
| CryptoEvent.VerificationRequestReceived
|
||||
| CryptoEvent.DeviceVerificationChanged
|
||||
| CryptoEvent.UserTrustStatusChanged
|
||||
| CryptoEvent.KeysChanged
|
||||
@@ -2196,10 +2204,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @experimental
|
||||
*
|
||||
* @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
|
||||
*
|
||||
* @returns a Promise which will resolve when the crypto layer has been
|
||||
* successfully initialised.
|
||||
*/
|
||||
public async initRustCrypto(): Promise<void> {
|
||||
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
|
||||
if (this.cryptoBackend) {
|
||||
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
|
||||
return;
|
||||
@@ -2229,11 +2239,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
deviceId,
|
||||
this.secretStorage,
|
||||
this.cryptoCallbacks,
|
||||
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
|
||||
);
|
||||
rustCrypto.supportedVerificationMethods = this.verificationMethods;
|
||||
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
|
||||
// re-emit the events emitted by the crypto impl
|
||||
this.reEmitter.reEmit(rustCrypto, [CryptoEvent.VerificationRequestReceived]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7635,7 +7651,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param bindThreepids - Set key 'email' to true to bind any email
|
||||
* threepid uses during registration in the identity server. Set 'msisdn' to
|
||||
* true to bind msisdn.
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a RegisterResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public register(
|
||||
@@ -7646,7 +7662,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
|
||||
guestAccessToken?: string,
|
||||
inhibitLogin?: boolean,
|
||||
): Promise<IAuthData> {
|
||||
): Promise<RegisterResponse> {
|
||||
// backwards compat
|
||||
if (bindThreepids === true) {
|
||||
bindThreepids = { email: true };
|
||||
@@ -7657,7 +7673,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
auth.session = sessionId;
|
||||
}
|
||||
|
||||
const params: IRegisterRequestParams = {
|
||||
const params: RegisterRequest = {
|
||||
auth: auth,
|
||||
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
|
||||
};
|
||||
@@ -7713,8 +7729,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* `{ user_id, device_id, access_token, home_server }`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public registerGuest({ body }: { body?: any } = {}): Promise<any> {
|
||||
// TODO: Types
|
||||
public registerGuest({ body }: { body?: RegisterRequest } = {}): Promise<RegisterResponse> {
|
||||
return this.registerRequest(body || {}, "guest");
|
||||
}
|
||||
|
||||
@@ -7724,7 +7739,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: to the /register response
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public registerRequest(data: IRegisterRequestParams, kind?: string): Promise<IAuthData> {
|
||||
public registerRequest(data: RegisterRequest, kind?: string): Promise<RegisterResponse> {
|
||||
const params: { kind?: string } = {};
|
||||
if (kind) {
|
||||
params.kind = kind;
|
||||
@@ -7777,23 +7792,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public login(loginType: string, data: any): Promise<any> {
|
||||
// TODO: Types
|
||||
const loginData = {
|
||||
type: loginType,
|
||||
};
|
||||
|
||||
// merge data into loginData
|
||||
Object.assign(loginData, data);
|
||||
|
||||
public login(loginType: LoginRequest["type"], data: Omit<LoginRequest, "type">): Promise<LoginResponse> {
|
||||
return this.http
|
||||
.authedRequest<{
|
||||
access_token?: string;
|
||||
user_id?: string;
|
||||
}>(Method.Post, "/login", undefined, loginData)
|
||||
.authedRequest<LoginResponse>(Method.Post, "/login", undefined, {
|
||||
...data,
|
||||
type: loginType,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.access_token && response.user_id) {
|
||||
this.http.opts.accessToken = response.access_token;
|
||||
@@ -7806,11 +7813,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public loginWithPassword(user: string, password: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public loginWithPassword(user: string, password: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.password", {
|
||||
user: user,
|
||||
password: password,
|
||||
@@ -7819,11 +7825,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param relayState - URL Callback after SAML2 Authentication
|
||||
* @returns Promise which resolves: TODO
|
||||
* @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<any> {
|
||||
// TODO: Types
|
||||
public loginWithSAML2(relayState: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.saml2", {
|
||||
relay_state: relayState,
|
||||
});
|
||||
@@ -7863,11 +7869,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param token - Login token previously received from homeserver
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public loginWithToken(token: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public loginWithToken(token: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.token", {
|
||||
token: token,
|
||||
});
|
||||
@@ -7911,7 +7916,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* to false.
|
||||
* @returns Promise which resolves: On success, the empty object
|
||||
*/
|
||||
public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> {
|
||||
public deactivateAccount(auth?: any, erase?: boolean): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
||||
const body: any = {};
|
||||
if (auth) {
|
||||
body.auth = auth;
|
||||
@@ -7932,7 +7937,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: On success, the token response
|
||||
* or UIA auth data.
|
||||
*/
|
||||
public async requestLoginToken(auth?: IAuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
// use capabilities to determine which revision of the MSC is being used
|
||||
const capabilities = await this.getCapabilities();
|
||||
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
|
||||
@@ -8572,7 +8577,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: to an empty object `{}`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public setPassword(authDict: IAuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
|
||||
public setPassword(authDict: AuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
|
||||
const path = "/account/password";
|
||||
const data = {
|
||||
auth: authDict,
|
||||
@@ -8630,7 +8635,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: result object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public deleteDevice(deviceId: string, auth?: IAuthDict): Promise<IAuthData | {}> {
|
||||
public deleteDevice(deviceId: string, auth?: AuthDict): Promise<{}> {
|
||||
const path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: deviceId,
|
||||
});
|
||||
@@ -8652,7 +8657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: result object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public deleteMultipleDevices(devices: string[], auth?: IAuthDict): Promise<IAuthData | {}> {
|
||||
public deleteMultipleDevices(devices: string[], auth?: AuthDict): Promise<{}> {
|
||||
const body: any = { devices };
|
||||
|
||||
if (auth) {
|
||||
@@ -8937,7 +8942,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Get, "/keys/changes", qps);
|
||||
}
|
||||
|
||||
public uploadDeviceSigningKeys(auth?: IAuthDict, keys?: CrossSigningKeys): Promise<{}> {
|
||||
public uploadDeviceSigningKeys(auth?: AuthDict, keys?: CrossSigningKeys): Promise<{}> {
|
||||
// API returns empty object
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, { auth });
|
||||
@@ -9085,6 +9090,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param msisdnToken - The MSISDN token, as enetered by the user.
|
||||
* @param identityAccessToken - The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
* Some legacy identity servers had no authentication here.
|
||||
*
|
||||
* @returns Promise which resolves: Object, containing success boolean.
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -9094,7 +9100,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
sid: string,
|
||||
clientSecret: string,
|
||||
msisdnToken: string,
|
||||
identityAccessToken: string,
|
||||
identityAccessToken: string | null,
|
||||
): Promise<{ success: boolean }> {
|
||||
const params = {
|
||||
sid: sid,
|
||||
@@ -9107,7 +9113,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
"/validate/msisdn/submitToken",
|
||||
params,
|
||||
IdentityPrefix.V2,
|
||||
identityAccessToken,
|
||||
identityAccessToken ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9149,8 +9155,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param identityAccessToken - The access token for the identity server.
|
||||
* @returns The hashing information for the identity server.
|
||||
*/
|
||||
public getIdentityHashDetails(identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public getIdentityHashDetails(identityAccessToken: string): Promise<{
|
||||
/**
|
||||
* The algorithms the server supports. Must contain at least sha256.
|
||||
*/
|
||||
algorithms: string[];
|
||||
/**
|
||||
* The pepper the client MUST use in hashing identifiers,
|
||||
* and MUST supply to the /lookup endpoint when performing lookups.
|
||||
*/
|
||||
lookup_pepper: string;
|
||||
}> {
|
||||
return this.http.idServerRequest(
|
||||
Method.Get,
|
||||
"/hash_details",
|
||||
@@ -9258,8 +9273,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* exists
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public async lookupThreePid(medium: string, address: string, identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public async lookupThreePid(
|
||||
medium: string,
|
||||
address: string,
|
||||
identityAccessToken: string,
|
||||
): Promise<
|
||||
| {
|
||||
address: string;
|
||||
medium: string;
|
||||
mxid: string;
|
||||
}
|
||||
| {}
|
||||
> {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
@@ -9295,8 +9320,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: Lookup results from IS.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public async bulkLookupThreePids(
|
||||
query: [string, string][],
|
||||
identityAccessToken: string,
|
||||
): Promise<{
|
||||
threepids: [medium: string, address: string, mxid: string][];
|
||||
}> {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
@@ -9334,8 +9363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: an object with account info.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public getIdentityAccount(identityAccessToken: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public getIdentityAccount(identityAccessToken: string): Promise<{ user_id: string }> {
|
||||
return this.http.idServerRequest(Method.Get, "/account", undefined, IdentityPrefix.V2, identityAccessToken);
|
||||
}
|
||||
|
||||
@@ -9428,7 +9456,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves to the result object
|
||||
*/
|
||||
public getThirdpartyUser(protocol: string, params: any): Promise<IThirdPartyUser[]> {
|
||||
// TODO: Types
|
||||
const path = utils.encodeUri("/thirdparty/user/$protocol", {
|
||||
$protocol: protocol,
|
||||
});
|
||||
|
||||
+20
-4
@@ -192,12 +192,17 @@ export interface CryptoApi {
|
||||
isSecretStorageReady(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Bootstrap the secret storage by creating a new secret storage key and store it in the secret storage.
|
||||
* Bootstrap the secret storage by creating a new secret storage key, add it in the secret storage and
|
||||
* store the cross signing keys in the secret storage.
|
||||
*
|
||||
* - Do nothing if an AES key is already stored in the secret storage and `setupNewKeyBackup` is not set;
|
||||
* - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`.
|
||||
* Only if `setupNewSecretStorage` is set or if there is no AES key in the secret storage
|
||||
* - Store this key in the secret storage and set it as the default key.
|
||||
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
|
||||
* - Store the cross signing keys in the secret storage if
|
||||
* - the cross signing is ready
|
||||
* - a new key was created during the previous step
|
||||
* - or the secret storage already contains the cross signing keys
|
||||
*
|
||||
* @param opts - Options object.
|
||||
*/
|
||||
@@ -251,7 +256,12 @@ export interface CryptoApi {
|
||||
/**
|
||||
* Send a verification request to our other devices.
|
||||
*
|
||||
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
||||
* This is normally used when the current device is new, and we want to ask another of our devices to cross-sign.
|
||||
*
|
||||
* If an all-devices verification is already in flight, returns it. Otherwise, initiates a new one.
|
||||
*
|
||||
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
|
||||
* MatrixClient.
|
||||
*
|
||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||
*/
|
||||
@@ -260,7 +270,13 @@ export interface CryptoApi {
|
||||
/**
|
||||
* Request an interactive verification with the given device.
|
||||
*
|
||||
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
||||
* This is normally used on one of our own devices, when the current device is already cross-signed, and we want to
|
||||
* validate another device.
|
||||
*
|
||||
* If a verification for this user/device is already in flight, returns it. Otherwise, initiates a new one.
|
||||
*
|
||||
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
|
||||
* MatrixClient.
|
||||
*
|
||||
* @param userId - ID of the owner of the device to verify
|
||||
* @param deviceId - ID of the device to verify
|
||||
|
||||
@@ -108,7 +108,7 @@ export interface VerificationRequest
|
||||
* Cancels the request, sending a cancellation to the other party
|
||||
*
|
||||
* @param params - Details for the cancellation, including `reason` (defaults to "User declined"), and `code`
|
||||
* (defaults to `m.user`).
|
||||
* (defaults to `m.user`). **Deprecated**: this parameter is ignored by the Rust cryptography implementation.
|
||||
*
|
||||
* @returns Promise which resolves when the event has been sent.
|
||||
*/
|
||||
@@ -128,9 +128,20 @@ export interface VerificationRequest
|
||||
* @param targetDevice - details of where to send the request to.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*
|
||||
* @deprecated Use {@link VerificationRequest#startVerification} instead.
|
||||
*/
|
||||
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*/
|
||||
startVerification(method: string): Promise<Verifier>;
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
* Only defined when the `phase` is Started.
|
||||
|
||||
@@ -194,9 +194,10 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
// check that the device that encrypted the event belongs to the user that the event claims it's from.
|
||||
//
|
||||
// To do this, we need to make sure that our device list is up-to-date. If the device is unknown, we can only
|
||||
// assume that the device logged out and accept it anyway. Some event handlers, such as secret sharing, may be
|
||||
// more strict and reject events that come from unknown devices.
|
||||
// If the device is unknown then we check that we don't have any pending key-query requests for the sender. If
|
||||
// after that the device is still unknown, then we can only assume that the device logged out and accept it
|
||||
// anyway. Some event handlers, such as secret sharing, may be more strict and reject events that come from
|
||||
// unknown devices.
|
||||
//
|
||||
// This is a defence against the following scenario:
|
||||
//
|
||||
@@ -204,14 +205,26 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
// * Mallory gets control of Alice's server, and sends a megolm session to Alice using her (Mallory's)
|
||||
// senderkey, but claiming to be from Bob.
|
||||
// * Mallory sends more events using that session, claiming to be from Bob.
|
||||
// * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those
|
||||
// events as verified even though the sender is forged.
|
||||
// * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those events as
|
||||
// verified even though the sender is forged.
|
||||
//
|
||||
// In practice, it's not clear that the js-sdk would behave that way, so this may be only a defence in depth.
|
||||
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
|
||||
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
|
||||
let senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
if (senderKeyUser === undefined || senderKeyUser === null) {
|
||||
// Wait for any pending key query fetches for the user to complete before trying the lookup again.
|
||||
try {
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
|
||||
} catch (e) {
|
||||
throw new DecryptionError("OLM_BAD_SENDER_CHECK_FAILED", "Could not verify sender identity", {
|
||||
sender: deviceKey,
|
||||
err: e as Error,
|
||||
});
|
||||
}
|
||||
|
||||
senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
}
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined && senderKeyUser !== null) {
|
||||
throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), {
|
||||
real_sender: senderKeyUser,
|
||||
});
|
||||
|
||||
@@ -118,12 +118,21 @@ export class BackupManager {
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
|
||||
// The backup manager will schedule backup of keys when active (`scheduleKeyBackupSend`), this allows cancel when client is stopped
|
||||
private clientRunning = true;
|
||||
|
||||
public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the backup manager from backing up keys and allow a clean shutdown.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.clientRunning = false;
|
||||
}
|
||||
|
||||
public get version(): string | undefined {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
@@ -439,6 +448,10 @@ export class BackupManager {
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay);
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send aborted, client stopped");
|
||||
return;
|
||||
}
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
for (;;) {
|
||||
if (!this.algorithm) {
|
||||
@@ -473,6 +486,11 @@ export class BackupManager {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||
}
|
||||
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send loop aborted, client stopped");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.sendingBackups = false;
|
||||
|
||||
+38
-15
@@ -91,6 +91,7 @@ import {
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeysOpts,
|
||||
VerificationRequest as CryptoApiVerificationRequest,
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
@@ -129,7 +130,12 @@ export function isCryptoAvailable(): boolean {
|
||||
return Boolean(global.Olm);
|
||||
}
|
||||
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
// minimum time between attempting to unwedge an Olm session, if we succeeded
|
||||
// in creating a new session
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
// minimum time between attempting to unwedge an Olm session, if we failed
|
||||
// to create a new session
|
||||
const FORCE_SESSION_RETRY_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
interface IInitOpts {
|
||||
exportedOlmDevice?: IExportedDevice;
|
||||
@@ -218,7 +224,16 @@ export enum CryptoEvent {
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
|
||||
/** @deprecated Use `VerificationRequestReceived`. */
|
||||
VerificationRequest = "crypto.verification.request",
|
||||
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*
|
||||
* The payload is a {@link Crypto.VerificationRequest}.
|
||||
*/
|
||||
VerificationRequestReceived = "crypto.verificationRequestReceived",
|
||||
|
||||
Warning = "crypto.warning",
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
@@ -280,8 +295,16 @@ export type CryptoEventHandlerMap = {
|
||||
) => void;
|
||||
/**
|
||||
* Fires when a key verification is requested.
|
||||
*
|
||||
* Deprecated: use `CryptoEvent.VerificationRequestReceived`.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
|
||||
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequestReceived]: (request: CryptoApiVerificationRequest) => void;
|
||||
|
||||
/**
|
||||
* Fires when the app may wish to warn the user about something related
|
||||
* the end-to-end crypto.
|
||||
@@ -372,7 +395,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// to avoid loading room members as long as possible.
|
||||
private roomDeviceTrackingState: { [roomId: string]: Promise<void> } = {};
|
||||
|
||||
// The timestamp of the last time we forced establishment
|
||||
// The timestamp of the minimum time at which we will retry forcing establishment
|
||||
// of a new session for each device, in milliseconds.
|
||||
// {
|
||||
// userId: {
|
||||
@@ -380,7 +403,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// },
|
||||
// }
|
||||
// Map: user Id → device Id → timestamp
|
||||
private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
|
||||
private forceNewSessionRetryTime: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => 0),
|
||||
);
|
||||
|
||||
@@ -1814,6 +1837,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
this.outgoingRoomKeyRequestManager.stop();
|
||||
this.deviceList.stop();
|
||||
this.dehydrationManager.stop();
|
||||
this.backupManager.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3541,6 +3565,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequestReceived, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3572,25 +3597,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return;
|
||||
}
|
||||
|
||||
// check when we last forced a new session with this device: if we've already done so
|
||||
// check when we can force a new session with this device: if we've already done so
|
||||
// recently, don't do it again.
|
||||
const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
|
||||
const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
|
||||
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
|
||||
const forceNewSessionRetryTimeDevices = this.forceNewSessionRetryTime.getOrCreate(sender);
|
||||
const forceNewSessionRetryTime = forceNewSessionRetryTimeDevices.getOrCreate(deviceKey);
|
||||
if (forceNewSessionRetryTime > Date.now()) {
|
||||
logger.debug(
|
||||
"New session already forced with device " +
|
||||
sender +
|
||||
":" +
|
||||
deviceKey +
|
||||
" at " +
|
||||
lastNewSessionForced +
|
||||
": not forcing another",
|
||||
`New session already forced with device ${sender}:${deviceKey}: ` +
|
||||
`not forcing another until at least ${new Date(forceNewSessionRetryTime).toUTCString()}`,
|
||||
);
|
||||
await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
|
||||
retryDecryption();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we don't retry to unwedge too soon even if we fail to create a new session
|
||||
forceNewSessionRetryTimeDevices.set(deviceKey, Date.now() + FORCE_SESSION_RETRY_INTERVAL_MS);
|
||||
|
||||
// establish a new olm session with this device since we're failing to decrypt messages
|
||||
// on a current session.
|
||||
// Note that an undecryptable message from another device could easily be spoofed -
|
||||
@@ -3611,7 +3634,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
const devicesByUser = new Map([[sender, [device]]]);
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
lastNewSessionDevices.set(deviceKey, Date.now());
|
||||
forceNewSessionRetryTimeDevices.set(deviceKey, Date.now() + MIN_FORCE_SESSION_INTERVAL_MS);
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
|
||||
@@ -18,6 +18,7 @@ limitations under the License.
|
||||
* QR code key verification.
|
||||
*/
|
||||
|
||||
import { crypto } from "../crypto";
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { newKeyMismatchError, newUserCancelledError } from "./Error";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
@@ -200,7 +201,7 @@ export class QRCodeData {
|
||||
|
||||
private static generateSharedSecret(): string {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,17 +52,22 @@ const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short
|
||||
|
||||
const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment");
|
||||
|
||||
// This list was generated from the data in the Matrix specification [1] with the following command:
|
||||
//
|
||||
// jq -r '.[] | " [\"" + .emoji + "\", \"" + (.description|ascii_downcase) + "\"], // " + (.number|tostring)' sas-emoji.json
|
||||
//
|
||||
// [1]: https://github.com/matrix-org/matrix-spec/blob/main/data-definitions/sas-emoji.json
|
||||
const emojiMapping: EmojiMapping[] = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
["🐎", "horse"], // 3
|
||||
["🦄", "unicorn"], // 4
|
||||
["🐷", "pig"], // 5
|
||||
["🐘", "elephant"], // 6
|
||||
["🐰", "rabbit"], // 7
|
||||
["🐼", "panda"], // 8
|
||||
["🐓", "rooster"], // 9
|
||||
["🐧", "penguin"], // 10
|
||||
["🐢", "turtle"], // 11
|
||||
["🐟", "fish"], // 12
|
||||
@@ -83,7 +88,7 @@ const emojiMapping: EmojiMapping[] = [
|
||||
["🍕", "pizza"], // 27
|
||||
["🎂", "cake"], // 28
|
||||
["❤️", "heart"], // 29
|
||||
["🙂", "smiley"], // 30
|
||||
["😀", "smiley"], // 30
|
||||
["🤖", "robot"], // 31
|
||||
["🎩", "hat"], // 32
|
||||
["👓", "glasses"], // 33
|
||||
@@ -113,7 +118,7 @@ const emojiMapping: EmojiMapping[] = [
|
||||
["🎸", "guitar"], // 57
|
||||
["🎺", "trumpet"], // 58
|
||||
["🔔", "bell"], // 59
|
||||
["⚓️", "anchor"], // 60
|
||||
["⚓", "anchor"], // 60
|
||||
["🎧", "headphones"], // 61
|
||||
["📁", "folder"], // 62
|
||||
["📌", "pin"], // 63
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
VerificationRequest as IVerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
VerificationRequestEventHandlerMap,
|
||||
Verifier,
|
||||
} from "../../../crypto-api/verification";
|
||||
|
||||
// backwards-compatibility exports
|
||||
@@ -458,6 +459,13 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return this._verifier!;
|
||||
}
|
||||
|
||||
public async startVerification(method: string): Promise<Verifier> {
|
||||
const verifier = this.beginKeyVerification(method);
|
||||
// kick off the verification in the background, but *don't* wait for to complete: we need to return the `Verifier`.
|
||||
verifier.verify();
|
||||
return verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* sends the initial .request event.
|
||||
* @returns resolves when the event has been sent.
|
||||
|
||||
+104
-74
@@ -21,6 +21,7 @@ import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { UIAResponse } from "./@types/uia";
|
||||
import { UserIdentifier } from "./@types/auth";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -51,22 +52,25 @@ export interface IStageStatus {
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#user-interactive-api-in-the-rest-api
|
||||
*/
|
||||
export interface IAuthData {
|
||||
// XXX: many of the fields here (`type`, `available_flows`, `required_stages`, etc) look like they
|
||||
// shouldn't be here. They aren't in the spec and it's unclear what they are supposed to do. Be wary of using them.
|
||||
/**
|
||||
* This is a session identifier that the client must pass back to the home server,
|
||||
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||
*/
|
||||
session?: string;
|
||||
type?: string;
|
||||
/**
|
||||
* A list of the stages the client has completed successfully
|
||||
*/
|
||||
completed?: string[];
|
||||
/**
|
||||
* A list of the login flows supported by the server for this API.
|
||||
*/
|
||||
flows?: UIAFlow[];
|
||||
available_flows?: UIAFlow[];
|
||||
stages?: string[];
|
||||
required_stages?: AuthType[];
|
||||
/**
|
||||
* Contains any information that the client will need to know in order to use a given type of authentication.
|
||||
* For each login type presented, that type may be present as a key in this dictionary.
|
||||
* For example, the public part of an OAuth client ID could be given here.
|
||||
*/
|
||||
params?: Record<string, Record<string, any>>;
|
||||
data?: Record<string, string>;
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
@@ -85,30 +89,62 @@ export enum AuthType {
|
||||
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||
}
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#password-based
|
||||
*/
|
||||
type PasswordDict = {
|
||||
type: AuthType.Password;
|
||||
identifier: UserIdentifier;
|
||||
password: string;
|
||||
session: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#google-recaptcha
|
||||
*/
|
||||
type RecaptchaDict = {
|
||||
type: AuthType.Recaptcha;
|
||||
response: string;
|
||||
session: string;
|
||||
};
|
||||
|
||||
interface ThreepidCreds {
|
||||
sid: string;
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#email-based-identity--homeserver
|
||||
*/
|
||||
type EmailIdentityDict = {
|
||||
type: AuthType.Email;
|
||||
threepid_creds: ThreepidCreds;
|
||||
/**
|
||||
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
|
||||
*/
|
||||
threepidCreds?: ThreepidCreds;
|
||||
session: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The parameters which are submitted as the `auth` dict in a UIA request
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
export interface IAuthDict {
|
||||
// [key: string]: any;
|
||||
type?: string;
|
||||
session?: string;
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
user?: string;
|
||||
identifier?: any;
|
||||
password?: string;
|
||||
response?: string;
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/element-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
// eslint-disable-next-line camelcase
|
||||
threepid_creds?: any;
|
||||
threepidCreds?: any;
|
||||
// For m.login.registration_token type
|
||||
token?: string;
|
||||
}
|
||||
export type AuthDict =
|
||||
| PasswordDict
|
||||
| RecaptchaDict
|
||||
| EmailIdentityDict
|
||||
| { type: Exclude<string, AuthType>; [key: string]: any }
|
||||
| {};
|
||||
|
||||
/**
|
||||
* Backwards compatible export
|
||||
* @deprecated in favour of AuthDict
|
||||
*/
|
||||
export type IAuthDict = AuthDict;
|
||||
|
||||
export class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
@@ -129,7 +165,7 @@ export class NoAuthFlowFoundError extends Error {
|
||||
*/
|
||||
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict) => Promise<UIAResponse<T>>) => Promise<T>;
|
||||
|
||||
interface IOpts {
|
||||
interface IOpts<T> {
|
||||
/**
|
||||
* A matrix client to use for the auth process
|
||||
*/
|
||||
@@ -170,7 +206,7 @@ interface IOpts {
|
||||
* The busyChanged callback should be used instead of the background flag.
|
||||
* Should return a promise which resolves to the successful response or rejects with a MatrixError.
|
||||
*/
|
||||
doRequest(auth: IAuthDict | null, background: boolean): Promise<IAuthData>;
|
||||
doRequest(auth: AuthDict | null, background: boolean): Promise<T>;
|
||||
/**
|
||||
* Called when the status of the UI auth changes,
|
||||
* ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
|
||||
@@ -215,21 +251,23 @@ interface IOpts {
|
||||
* submitAuthDict.
|
||||
*
|
||||
* @param opts - options object
|
||||
* @typeParam T - the return type of the request when it is successful
|
||||
*/
|
||||
export class InteractiveAuth {
|
||||
export class InteractiveAuth<T> {
|
||||
private readonly matrixClient: MatrixClient;
|
||||
private readonly inputs: IInputs;
|
||||
private readonly clientSecret: string;
|
||||
private readonly requestCallback: IOpts["doRequest"];
|
||||
private readonly busyChangedCallback?: IOpts["busyChanged"];
|
||||
private readonly stateUpdatedCallback: IOpts["stateUpdated"];
|
||||
private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
|
||||
private readonly requestCallback: IOpts<T>["doRequest"];
|
||||
private readonly busyChangedCallback?: IOpts<T>["busyChanged"];
|
||||
private readonly stateUpdatedCallback: IOpts<T>["stateUpdated"];
|
||||
private readonly requestEmailTokenCallback: IOpts<T>["requestEmailToken"];
|
||||
private readonly supportedStages?: Set<string>;
|
||||
|
||||
// The current latest data received from the server during the user interactive auth flow.
|
||||
private data: IAuthData;
|
||||
private emailSid?: string;
|
||||
private requestingEmailToken = false;
|
||||
private attemptAuthDeferred: IDeferred<IAuthData> | null = null;
|
||||
private attemptAuthDeferred: IDeferred<T> | null = null;
|
||||
private chosenFlow: UIAFlow | null = null;
|
||||
private currentStage: string | null = null;
|
||||
|
||||
@@ -239,9 +277,9 @@ export class InteractiveAuth {
|
||||
// the promise the will resolve/reject when it completes
|
||||
private submitPromise: Promise<void> | null = null;
|
||||
|
||||
public constructor(opts: IOpts) {
|
||||
public constructor(opts: IOpts<T>) {
|
||||
this.matrixClient = opts.matrixClient;
|
||||
this.data = opts.authData || {};
|
||||
this.data = opts.authData || { flows: [] };
|
||||
this.requestCallback = opts.doRequest;
|
||||
this.busyChangedCallback = opts.busyChanged;
|
||||
// startAuthStage included for backwards compat
|
||||
@@ -262,7 +300,7 @@ export class InteractiveAuth {
|
||||
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
|
||||
* no suitable authentication flow can be found
|
||||
*/
|
||||
public attemptAuth(): Promise<IAuthData> {
|
||||
public async attemptAuth(): Promise<T> {
|
||||
// This promise will be quite long-lived and will resolve when the
|
||||
// request is authenticated and completes successfully.
|
||||
this.attemptAuthDeferred = defer();
|
||||
@@ -270,10 +308,10 @@ export class InteractiveAuth {
|
||||
const promise = this.attemptAuthDeferred.promise;
|
||||
|
||||
// if we have no flows, try a request to acquire the flows
|
||||
if (!this.data?.flows) {
|
||||
if (!(this.data as IAuthData)?.flows?.length) {
|
||||
this.busyChangedCallback?.(true);
|
||||
// use the existing sessionId, if one is present.
|
||||
const auth = this.data.session ? { session: this.data.session } : null;
|
||||
const auth = (this.data as IAuthData).session ? { session: (this.data as IAuthData).session } : null;
|
||||
this.doRequest(auth).finally(() => {
|
||||
this.busyChangedCallback?.(false);
|
||||
});
|
||||
@@ -290,7 +328,7 @@ export class InteractiveAuth {
|
||||
* be resolved.
|
||||
*/
|
||||
public async poll(): Promise<void> {
|
||||
if (!this.data.session) return;
|
||||
if (!(this.data as IAuthData).session) return;
|
||||
// likewise don't poll if there is no auth session in progress
|
||||
if (!this.attemptAuthDeferred) return;
|
||||
// if we currently have a request in flight, there's no point making
|
||||
@@ -330,7 +368,7 @@ export class InteractiveAuth {
|
||||
* @returns session id
|
||||
*/
|
||||
public getSessionId(): string | undefined {
|
||||
return this.data?.session;
|
||||
return (this.data as IAuthData)?.session;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +388,7 @@ export class InteractiveAuth {
|
||||
* @returns any parameters from the server for this stage
|
||||
*/
|
||||
public getStageParams(loginType: string): Record<string, any> | undefined {
|
||||
return this.data.params?.[loginType];
|
||||
return (this.data as IAuthData)?.params?.[loginType];
|
||||
}
|
||||
|
||||
public getChosenFlow(): UIAFlow | null {
|
||||
@@ -391,9 +429,9 @@ export class InteractiveAuth {
|
||||
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth: IAuthDict;
|
||||
if (this.data.session) {
|
||||
if ((this.data as IAuthData)?.session) {
|
||||
auth = {
|
||||
session: this.data.session,
|
||||
session: (this.data as IAuthData).session,
|
||||
};
|
||||
Object.assign(auth, authData);
|
||||
} else {
|
||||
@@ -451,7 +489,7 @@ export class InteractiveAuth {
|
||||
this.inputs.emailAddress!,
|
||||
this.clientSecret,
|
||||
this.emailAttempt++,
|
||||
this.data.session!,
|
||||
(this.data as IAuthData).session!,
|
||||
);
|
||||
this.emailSid = requestTokenResult.sid;
|
||||
logger.trace("Email token request succeeded");
|
||||
@@ -480,10 +518,12 @@ export class InteractiveAuth {
|
||||
this.attemptAuthDeferred!.resolve(result);
|
||||
this.attemptAuthDeferred = null;
|
||||
} catch (error) {
|
||||
const matrixError = error instanceof MatrixError ? error : null;
|
||||
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = (<MatrixError>error).data?.flows ?? null;
|
||||
const haveFlows = this.data.flows || Boolean(errorFlows);
|
||||
if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
|
||||
const errorFlows = matrixError?.data?.flows ?? null;
|
||||
const haveFlows = (this.data as IAuthData)?.flows || Boolean(errorFlows);
|
||||
if (!matrixError || matrixError.httpStatus !== 401 || !matrixError.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
this.attemptAuthDeferred?.reject(error);
|
||||
@@ -494,24 +534,22 @@ export class InteractiveAuth {
|
||||
logger.log("Background poll request failed doing UI auth: ignoring", error);
|
||||
}
|
||||
}
|
||||
if (!(<MatrixError>error).data) {
|
||||
(<MatrixError>error).data = {};
|
||||
if (matrixError && !matrixError.data) {
|
||||
matrixError.data = {};
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
// copy over the ones we have. Synapse sometimes sends responses without
|
||||
// any UI auth data (eg. when polling for email validation, if the email
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (
|
||||
!(<MatrixError>error).data.flows &&
|
||||
!(<MatrixError>error).data.completed &&
|
||||
!(<MatrixError>error).data.session
|
||||
) {
|
||||
(<MatrixError>error).data.flows = this.data.flows;
|
||||
(<MatrixError>error).data.completed = this.data.completed;
|
||||
(<MatrixError>error).data.session = this.data.session;
|
||||
if (matrixError && !matrixError.data.flows && !matrixError.data.completed && !matrixError.data.session) {
|
||||
matrixError.data.flows = (this.data as IAuthData).flows;
|
||||
matrixError.data.completed = (this.data as IAuthData).completed;
|
||||
matrixError.data.session = (this.data as IAuthData).session;
|
||||
}
|
||||
if (matrixError) {
|
||||
this.data = matrixError.data as IAuthData;
|
||||
}
|
||||
this.data = (<MatrixError>error).data;
|
||||
try {
|
||||
this.startNextAuthStage();
|
||||
} catch (e) {
|
||||
@@ -563,14 +601,6 @@ export class InteractiveAuth {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.data?.errcode || this.data?.error) {
|
||||
this.stateUpdatedCallback(nextStage, {
|
||||
errcode: this.data?.errcode || "",
|
||||
error: this.data?.error || "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
|
||||
}
|
||||
|
||||
@@ -618,7 +648,7 @@ export class InteractiveAuth {
|
||||
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
private chooseFlow(): UIAFlow {
|
||||
const flows = this.data.flows || [];
|
||||
const flows = (this.data as IAuthData)?.flows || [];
|
||||
|
||||
// we've been given an email or we've already done an email part
|
||||
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
|
||||
@@ -659,7 +689,7 @@ export class InteractiveAuth {
|
||||
* @returns login type
|
||||
*/
|
||||
private firstUncompletedStage(flow: UIAFlow): AuthType | string | undefined {
|
||||
const completed = this.data.completed || [];
|
||||
const completed = (this.data as IAuthData)?.completed || [];
|
||||
return flow.stages.find((stageType) => !completed.includes(stageType));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -38,7 +38,11 @@ log.methodFactory = function (methodName, logLevel, loggerName) {
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-invalid-this */
|
||||
const supportedByConsole =
|
||||
methodName === "error" || methodName === "warn" || methodName === "trace" || methodName === "info";
|
||||
methodName === "error" ||
|
||||
methodName === "warn" ||
|
||||
methodName === "trace" ||
|
||||
methodName === "info" ||
|
||||
methodName === "debug";
|
||||
/* eslint-disable no-console */
|
||||
if (supportedByConsole) {
|
||||
return console[methodName](...args);
|
||||
|
||||
@@ -721,13 +721,17 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
}
|
||||
|
||||
const eventId = event.getId()!;
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
// Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
|
||||
// threaded message should not be in the main timeline).
|
||||
//
|
||||
// We can only run this check for timelines with a `room` because `canContain`
|
||||
// requires it
|
||||
if (this.room && !this.canContain(event)) {
|
||||
let eventDebugString = `event=${event.getId()}`;
|
||||
let eventDebugString = `event=${eventId}`;
|
||||
if (event.threadRootId) {
|
||||
eventDebugString += `(belongs to thread=${event.threadRootId})`;
|
||||
}
|
||||
@@ -738,7 +742,6 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = event.getId()!;
|
||||
timeline.addEvent(event, {
|
||||
toStartOfTimeline,
|
||||
roomState,
|
||||
@@ -746,9 +749,6 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
});
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
const data: IRoomTimelineData = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||
@@ -778,22 +778,26 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
*/
|
||||
public insertEventIntoTimeline(event: MatrixEvent, timeline: EventTimeline, roomState: RoomState): void {
|
||||
if (timeline.getTimelineSet() !== this) {
|
||||
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
}
|
||||
|
||||
const eventId = event.getId()!;
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
// Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
|
||||
// threaded message should not be in the main timeline).
|
||||
//
|
||||
// We can only run this check for timelines with a `room` because `canContain`
|
||||
// requires it
|
||||
if (this.room && !this.canContain(event)) {
|
||||
let eventDebugString = `event=${event.getId()}`;
|
||||
let eventDebugString = `event=${eventId}`;
|
||||
if (event.threadRootId) {
|
||||
eventDebugString += `(belongs to thread=${event.threadRootId})`;
|
||||
}
|
||||
logger.warn(
|
||||
`EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` +
|
||||
`EventTimelineSet.insertEventIntoTimeline: Ignoring ${eventDebugString} that does not belong ` +
|
||||
`in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`,
|
||||
);
|
||||
return;
|
||||
@@ -830,13 +834,9 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
// If we got to the end of the loop, insertIndex points at the end of
|
||||
// the list.
|
||||
|
||||
const eventId = event.getId()!;
|
||||
timeline.insertEvent(event, insertIndex, roomState);
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
const data: IRoomTimelineData = {
|
||||
timeline: timeline,
|
||||
liveEvent: timeline == this.liveTimeline,
|
||||
|
||||
+29
-10
@@ -29,12 +29,13 @@ import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
ToDeviceMessageId,
|
||||
UNSIGNED_THREAD_ID_FIELD,
|
||||
} from "../@types/event";
|
||||
import { Crypto } from "../crypto";
|
||||
import { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent, ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent, ThreadEventHandlerMap } from "./thread";
|
||||
import { IActionsObject } from "../pushprocessor";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { MatrixError } from "../http-api";
|
||||
@@ -62,10 +63,12 @@ export interface IContent {
|
||||
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
|
||||
|
||||
export interface IUnsigned {
|
||||
[key: string]: any;
|
||||
"age"?: number;
|
||||
"prev_sender"?: string;
|
||||
"prev_content"?: IContent;
|
||||
"redacted_because"?: IEvent;
|
||||
"replaces_state"?: string;
|
||||
"transaction_id"?: string;
|
||||
"invite_room_state"?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
@@ -517,16 +520,15 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* ```
|
||||
*/
|
||||
public getDetails(): string {
|
||||
let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`;
|
||||
const room = this.getRoomId();
|
||||
if (room) {
|
||||
details += ` room=${room}`;
|
||||
// in-room event
|
||||
return `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()} room=${room} ts=${this.getDate()?.toISOString()}`;
|
||||
} else {
|
||||
// to-device event
|
||||
const msgid = this.getContent()[ToDeviceMessageId];
|
||||
return `msgid=${msgid} type=${this.getWireType()} sender=${this.getSender()}`;
|
||||
}
|
||||
const date = this.getDate();
|
||||
if (date) {
|
||||
details += ` ts=${date.toISOString()}`;
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,6 +578,10 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string | undefined {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return undefined;
|
||||
}
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||
return relatesTo.event_id;
|
||||
@@ -597,6 +603,11 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* A helper to check if an event is a thread's head or not
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
@@ -1365,8 +1376,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const relation = this.getWireContent()?.["m.relates_to"];
|
||||
if (this.isState() && relation?.rel_type === RelationType.Replace) {
|
||||
// State events cannot be m.replace relations
|
||||
if (
|
||||
this.isState() &&
|
||||
!!relation?.rel_type &&
|
||||
([RelationType.Replace, RelationType.Thread] as string[]).includes(relation.rel_type)
|
||||
) {
|
||||
// State events cannot be m.replace or m.thread relations
|
||||
return false;
|
||||
}
|
||||
return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true));
|
||||
@@ -1618,6 +1633,10 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @param thread - the thread
|
||||
*/
|
||||
public setThread(thread?: Thread): void {
|
||||
// don't allow state events to be threaded as per the spec
|
||||
if (this.isState()) {
|
||||
return;
|
||||
}
|
||||
if (this.thread) {
|
||||
this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]);
|
||||
}
|
||||
|
||||
@@ -309,4 +309,13 @@ export abstract class ReadReceipt<
|
||||
// We don't know if the user has read it, so assume not.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent unthreaded receipt for a given user
|
||||
* @param userId - the MxID of the User
|
||||
* @returns an unthreaded Receipt. Can be undefined if receipts have been disabled
|
||||
* or a user chooses to use private read receipts (or we have simply not received
|
||||
* a receipt from this user yet).
|
||||
*/
|
||||
public abstract getLastUnthreadedReceiptFor(userId: string): Receipt | undefined;
|
||||
}
|
||||
|
||||
+58
-10
@@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
import { logger } from "../logger";
|
||||
import { ReadReceipt } from "./read-receipt";
|
||||
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
|
||||
import { CachedReceiptStructure, Receipt, ReceiptType } from "../@types/read_receipts";
|
||||
import { Feature, ServerSupport } from "../feature";
|
||||
|
||||
export enum ThreadEvent {
|
||||
@@ -98,6 +98,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
private readonly pendingEventOrdering: PendingEventOrdering;
|
||||
private processRootEventPromise?: Promise<void>;
|
||||
|
||||
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||
/**
|
||||
@@ -134,6 +135,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
this.room.on(RoomEvent.Redaction, this.onRedaction);
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
|
||||
this.room.on(RoomEvent.TimelineReset, this.onTimelineReset);
|
||||
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);
|
||||
|
||||
this.processReceipts(opts.receipts);
|
||||
@@ -144,6 +146,12 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this.setEventMetadata(this.rootEvent);
|
||||
}
|
||||
|
||||
private onTimelineReset = async (): Promise<void> => {
|
||||
// We hit a gappy sync, ask the server for an update
|
||||
await this.processRootEventPromise;
|
||||
this.processRootEventPromise = undefined;
|
||||
};
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
this.rootEvent = this.room.findEventById(this.id);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
@@ -197,6 +205,11 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this._currentUserParticipated = false;
|
||||
this.emit(ThreadEvent.Delete, this);
|
||||
} else {
|
||||
if (this.lastEvent?.getId() === event.getAssociatedId()) {
|
||||
// XXX: If our last event got redacted we query the server for the last event once again
|
||||
await this.processRootEventPromise;
|
||||
this.processRootEventPromise = undefined;
|
||||
}
|
||||
await this.updateThreadMetadata();
|
||||
}
|
||||
};
|
||||
@@ -212,6 +225,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
if (sender && room && this.shouldSendLocalEchoReceipt(sender, event)) {
|
||||
room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
|
||||
}
|
||||
if (event.getId() !== this.id && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
}
|
||||
}
|
||||
this.onEcho(event, toStartOfTimeline ?? false);
|
||||
};
|
||||
@@ -245,6 +261,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
await this.updateThreadMetadata();
|
||||
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits
|
||||
if (toStartOfTimeline) return; // ignore messages added to the start of the timeline
|
||||
// Clear the lastEvent and instead start tracking locally using lastReply
|
||||
this.lastEvent = undefined;
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
};
|
||||
|
||||
@@ -308,6 +326,11 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
|
||||
this.setEventMetadata(event);
|
||||
|
||||
if (!this.initialEventsFetched && !toStartOfTimeline && event.getId() === this.id) {
|
||||
// We're loading the thread organically
|
||||
this.initialEventsFetched = true;
|
||||
}
|
||||
|
||||
const lastReply = this.lastReply();
|
||||
const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp;
|
||||
|
||||
@@ -351,10 +374,14 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
return;
|
||||
}
|
||||
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
if (
|
||||
event.getId() !== this.id &&
|
||||
event.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
!toStartOfTimeline &&
|
||||
isNewestReply
|
||||
) {
|
||||
// Clear the last event as we have the latest end of the timeline
|
||||
this.lastEvent = undefined;
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
@@ -475,18 +502,26 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
}
|
||||
}
|
||||
|
||||
private async updateThreadMetadata(): Promise<void> {
|
||||
this.updatePendingReplyCount();
|
||||
|
||||
private async updateThreadFromRootEvent(): Promise<void> {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
// Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we
|
||||
// don't want the thread preview to be empty if we can avoid it
|
||||
if (!this.initialEventsFetched) {
|
||||
if (!this.initialEventsFetched && !this.lastEvent) {
|
||||
await this.processRootEvent();
|
||||
}
|
||||
await this.fetchRootEvent();
|
||||
}
|
||||
await this.processRootEvent();
|
||||
}
|
||||
|
||||
private async updateThreadMetadata(): Promise<void> {
|
||||
this.updatePendingReplyCount();
|
||||
|
||||
if (!this.processRootEventPromise) {
|
||||
// We only want to do this once otherwise we end up rolling back to the last unsigned summary we have for the thread
|
||||
this.processRootEventPromise = this.updateThreadFromRootEvent();
|
||||
}
|
||||
await this.processRootEventPromise;
|
||||
|
||||
if (!this.initialEventsFetched) {
|
||||
this.initialEventsFetched = true;
|
||||
@@ -572,7 +607,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
/**
|
||||
* Return last reply to the thread, if known.
|
||||
*/
|
||||
public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null {
|
||||
public lastReply(
|
||||
matches: (ev: MatrixEvent) => boolean = (ev): boolean => ev.isRelation(RelationType.Thread),
|
||||
): MatrixEvent | null {
|
||||
for (let i = this.timeline.length - 1; i >= 0; i--) {
|
||||
const event = this.timeline[i];
|
||||
if (matches(event)) {
|
||||
@@ -711,6 +748,17 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public setUnread(type: NotificationCountType, count: number): void {
|
||||
return this.room.setThreadUnreadNotificationCount(this.id, type, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent unthreaded receipt for a given user
|
||||
* @param userId - the MxID of the User
|
||||
* @returns an unthreaded Receipt. Can be undefined if receipts have been disabled
|
||||
* or a user chooses to use private read receipts (or we have simply not received
|
||||
* a receipt from this user yet).
|
||||
*/
|
||||
public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined {
|
||||
return this.room.getLastUnthreadedReceiptFor(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { Method } from "../http-api";
|
||||
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
||||
import { logger } from "../logger";
|
||||
import { randomString } from "../randomstring";
|
||||
import { OidcError } from "./error";
|
||||
import { validateIdToken, ValidatedIssuerConfig } from "./validate";
|
||||
|
||||
/**
|
||||
* Authorization parameters which are used in the authentication request of an OIDC auth code flow.
|
||||
*
|
||||
* See https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters.
|
||||
*/
|
||||
export type AuthorizationParams = {
|
||||
state: string;
|
||||
scope: string;
|
||||
redirectUri: string;
|
||||
codeVerifier: string;
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
const generateScope = (): string => {
|
||||
const deviceId = randomString(10);
|
||||
return `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${deviceId}`;
|
||||
};
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc7636
|
||||
const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
|
||||
if (!subtleCrypto) {
|
||||
// @TODO(kerrya) should this be allowed? configurable?
|
||||
logger.warn("A secure context is required to generate code challenge. Using plain text code challenge");
|
||||
return codeVerifier;
|
||||
}
|
||||
const utf8 = new TextEncoder().encode(codeVerifier);
|
||||
|
||||
const digest = await subtleCrypto.digest("SHA-256", utf8);
|
||||
|
||||
return btoa(String.fromCharCode(...new Uint8Array(digest)))
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate authorization params to pass to {@link generateAuthorizationUrl}.
|
||||
*
|
||||
* Used as part of an authorization code OIDC flow: see https://openid.net/specs/openid-connect-basic-1_0.html#CodeFlow.
|
||||
*
|
||||
* @param redirectUri - absolute url for OP to redirect to after authorization
|
||||
* @returns AuthorizationParams
|
||||
*/
|
||||
export const generateAuthorizationParams = ({ redirectUri }: { redirectUri: string }): AuthorizationParams => ({
|
||||
scope: generateScope(),
|
||||
redirectUri,
|
||||
state: randomString(8),
|
||||
nonce: randomString(8),
|
||||
codeVerifier: randomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate a URL to attempt authorization with the OP
|
||||
* See https://openid.net/specs/openid-connect-basic-1_0.html#CodeRequest
|
||||
* @param authorizationUrl - endpoint to attempt authorization with the OP
|
||||
* @param clientId - id of this client as registered with the OP
|
||||
* @param authorizationParams - params to be used in the url
|
||||
* @returns a Promise with the url as a string
|
||||
*/
|
||||
export const generateAuthorizationUrl = async (
|
||||
authorizationUrl: string,
|
||||
clientId: string,
|
||||
{ scope, redirectUri, state, nonce, codeVerifier }: AuthorizationParams,
|
||||
): Promise<string> => {
|
||||
const url = new URL(authorizationUrl);
|
||||
url.searchParams.append("response_mode", "query");
|
||||
url.searchParams.append("response_type", "code");
|
||||
url.searchParams.append("redirect_uri", redirectUri);
|
||||
url.searchParams.append("client_id", clientId);
|
||||
url.searchParams.append("state", state);
|
||||
url.searchParams.append("scope", scope);
|
||||
url.searchParams.append("nonce", nonce);
|
||||
|
||||
url.searchParams.append("code_challenge_method", "S256");
|
||||
url.searchParams.append("code_challenge", await generateCodeChallenge(codeVerifier));
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The expected response type from the token endpoint during authorization code flow
|
||||
* Normalized to always use capitalized 'Bearer' for token_type
|
||||
*
|
||||
* See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4,
|
||||
* https://openid.net/specs/openid-connect-basic-1_0.html#TokenOK.
|
||||
*/
|
||||
export type BearerTokenResponse = {
|
||||
token_type: "Bearer";
|
||||
access_token: string;
|
||||
scope: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
id_token?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Expected response type from the token endpoint during authorization code flow
|
||||
* as it comes over the wire.
|
||||
* Should be normalized to use capital case 'Bearer' for token_type property
|
||||
*
|
||||
* See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4,
|
||||
* https://openid.net/specs/openid-connect-basic-1_0.html#TokenOK.
|
||||
*/
|
||||
type WireBearerTokenResponse = BearerTokenResponse & {
|
||||
token_type: "Bearer" | "bearer";
|
||||
};
|
||||
|
||||
const isResponseObject = (response: unknown): response is Record<string, unknown> =>
|
||||
!!response && typeof response === "object";
|
||||
|
||||
/**
|
||||
* Normalize token_type to use capital case to make consuming the token response easier
|
||||
* token_type is case insensitive, and it is spec-compliant for OPs to return token_type: "bearer"
|
||||
* Later, when used in auth headers it is case sensitive and must be Bearer
|
||||
* See: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
|
||||
*
|
||||
* @param response - validated token response
|
||||
* @returns response with token_type set to 'Bearer'
|
||||
*/
|
||||
const normalizeBearerTokenResponseTokenType = (response: WireBearerTokenResponse): BearerTokenResponse => ({
|
||||
...response,
|
||||
token_type: "Bearer",
|
||||
});
|
||||
|
||||
const isValidBearerTokenResponse = (response: unknown): response is WireBearerTokenResponse =>
|
||||
isResponseObject(response) &&
|
||||
typeof response["token_type"] === "string" &&
|
||||
// token_type is case insensitive, some OPs return `token_type: "bearer"`
|
||||
response["token_type"].toLowerCase() === "bearer" &&
|
||||
typeof response["access_token"] === "string" &&
|
||||
(!("refresh_token" in response) || typeof response["refresh_token"] === "string") &&
|
||||
(!("expires_in" in response) || typeof response["expires_in"] === "number");
|
||||
|
||||
/**
|
||||
* Attempt to exchange authorization code for bearer token.
|
||||
*
|
||||
* Takes the authorization code returned by the OpenID Provider via the authorization URL, and makes a
|
||||
* request to the Token Endpoint, to obtain the access token, refresh token, etc.
|
||||
*
|
||||
* @param code - authorization code as returned by OP during authorization
|
||||
* @param storedAuthorizationParams - stored params from start of oidc login flow
|
||||
* @returns valid bearer token response
|
||||
* @throws when request fails, or returned token response is invalid
|
||||
*/
|
||||
export const completeAuthorizationCodeGrant = async (
|
||||
code: string,
|
||||
{
|
||||
clientId,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
delegatedAuthConfig,
|
||||
nonce,
|
||||
}: {
|
||||
clientId: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig;
|
||||
nonce: string;
|
||||
},
|
||||
): Promise<BearerTokenResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("client_id", clientId);
|
||||
params.append("code_verifier", codeVerifier);
|
||||
params.append("redirect_uri", redirectUri);
|
||||
params.append("code", code);
|
||||
const metadata = params.toString();
|
||||
|
||||
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
||||
|
||||
const response = await fetch(delegatedAuthConfig.tokenEndpoint, {
|
||||
method: Method.Post,
|
||||
headers,
|
||||
body: metadata,
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(OidcError.CodeExchangeFailed);
|
||||
}
|
||||
|
||||
const token = await response.json();
|
||||
|
||||
if (isValidBearerTokenResponse(token)) {
|
||||
// throws when token is invalid
|
||||
validateIdToken(token.id_token, delegatedAuthConfig.issuer, clientId, nonce);
|
||||
return normalizeBearerTokenResponseTokenType(token);
|
||||
}
|
||||
|
||||
throw new Error(OidcError.InvalidBearerTokenResponse);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum OidcError {
|
||||
NotSupported = "OIDC authentication not supported",
|
||||
Misconfigured = "OIDC is misconfigured",
|
||||
General = "Something went wrong with OIDC discovery",
|
||||
OpSupport = "Configured OIDC OP does not support required functions",
|
||||
DynamicRegistrationNotSupported = "Dynamic registration not supported",
|
||||
DynamicRegistrationFailed = "Dynamic registration failed",
|
||||
DynamicRegistrationInvalid = "Dynamic registration invalid response",
|
||||
CodeExchangeFailed = "Failed to exchange code for token",
|
||||
InvalidBearerTokenResponse = "Invalid bearer token response",
|
||||
InvalidIdToken = "Invalid ID token",
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { OidcError } from "./error";
|
||||
import { Method } from "../http-api";
|
||||
import { logger } from "../logger";
|
||||
import { ValidatedIssuerConfig } from "./validate";
|
||||
|
||||
/**
|
||||
* Client metadata passed to registration endpoint
|
||||
*/
|
||||
export type OidcRegistrationClientMetadata = {
|
||||
clientName: string;
|
||||
clientUri: string;
|
||||
redirectUris: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the client registration request
|
||||
* @param registrationEndpoint - URL as returned from issuer ./well-known/openid-configuration
|
||||
* @param clientMetadata - registration metadata
|
||||
* @returns resolves to the registered client id when registration is successful
|
||||
* @throws when registration request fails, or response is invalid
|
||||
*/
|
||||
const doRegistration = async (
|
||||
registrationEndpoint: string,
|
||||
clientMetadata: OidcRegistrationClientMetadata,
|
||||
): Promise<string> => {
|
||||
// https://openid.net/specs/openid-connect-registration-1_0.html
|
||||
const metadata = {
|
||||
client_name: clientMetadata.clientName,
|
||||
client_uri: clientMetadata.clientUri,
|
||||
response_types: ["code"],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
redirect_uris: clientMetadata.redirectUris,
|
||||
id_token_signed_response_alg: "RS256",
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
};
|
||||
const headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(registrationEndpoint, {
|
||||
method: Method.Post,
|
||||
headers,
|
||||
body: JSON.stringify(metadata),
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(OidcError.DynamicRegistrationFailed);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
const clientId = body["client_id"];
|
||||
if (!clientId || typeof clientId !== "string") {
|
||||
throw new Error(OidcError.DynamicRegistrationInvalid);
|
||||
}
|
||||
|
||||
return clientId;
|
||||
} catch (error) {
|
||||
if (Object.values(OidcError).includes((error as Error).message as OidcError)) {
|
||||
throw error;
|
||||
} else {
|
||||
logger.error("Dynamic registration request failed", error);
|
||||
throw new Error(OidcError.DynamicRegistrationFailed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts dynamic registration against the configured registration endpoint
|
||||
* @param delegatedAuthConfig - Auth config from ValidatedServerConfig
|
||||
* @param clientName - Client name to register with the OP, eg 'Element'
|
||||
* @param baseUrl - URL of the home page of the Client, eg 'https://app.element.io/'
|
||||
* @returns Promise<string> resolved with registered clientId
|
||||
* @throws when registration is not supported, on failed request or invalid response
|
||||
*/
|
||||
export const registerOidcClient = async (
|
||||
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig,
|
||||
clientName: string,
|
||||
baseUrl: string,
|
||||
): Promise<string> => {
|
||||
const clientMetadata = {
|
||||
clientName,
|
||||
clientUri: baseUrl,
|
||||
redirectUris: [baseUrl],
|
||||
};
|
||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
const clientId = await doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata);
|
||||
|
||||
return clientId;
|
||||
};
|
||||
+94
-11
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { OidcError } from "./error";
|
||||
|
||||
export enum OidcDiscoveryError {
|
||||
NotSupported = "OIDC authentication not supported",
|
||||
Misconfigured = "OIDC is misconfigured",
|
||||
General = "Something went wrong with OIDC discovery",
|
||||
OpSupport = "Configured OIDC OP does not support required functions",
|
||||
}
|
||||
/**
|
||||
* re-export for backwards compatibility
|
||||
* @deprecated use OidcError
|
||||
*/
|
||||
export { OidcError as OidcDiscoveryError };
|
||||
|
||||
export type ValidatedIssuerConfig = {
|
||||
authorizationEndpoint: string;
|
||||
@@ -41,7 +43,7 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown);
|
||||
|
||||
if (!authentication) {
|
||||
throw new Error(OidcDiscoveryError.NotSupported);
|
||||
throw new Error(OidcError.NotSupported);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -54,7 +56,7 @@ export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): ID
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(OidcDiscoveryError.Misconfigured);
|
||||
throw new Error(OidcError.Misconfigured);
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
@@ -83,7 +85,7 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates issue `.well-known/openid-configuration`
|
||||
* Validates issuer `.well-known/openid-configuration`
|
||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
* validates that OP is compatible with Element's OIDC flow
|
||||
* @param wellKnown - json object
|
||||
@@ -93,7 +95,7 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => {
|
||||
if (!isRecord(wellKnown)) {
|
||||
logger.error("Issuer configuration not found or malformed");
|
||||
throw new Error(OidcDiscoveryError.OpSupport);
|
||||
throw new Error(OidcError.OpSupport);
|
||||
}
|
||||
|
||||
const isInvalid = [
|
||||
@@ -114,5 +116,86 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
|
||||
}
|
||||
|
||||
logger.error("Issuer configuration not valid");
|
||||
throw new Error(OidcDiscoveryError.OpSupport);
|
||||
throw new Error(OidcError.OpSupport);
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard JWT claims.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
||||
*/
|
||||
interface JwtClaims {
|
||||
[claim: string]: unknown;
|
||||
/** The "iss" (issuer) claim identifies the principal that issued the JWT. */
|
||||
iss?: string;
|
||||
/** The "sub" (subject) claim identifies the principal that is the subject of the JWT. */
|
||||
sub?: string;
|
||||
/** The "aud" (audience) claim identifies the recipients that the JWT is intended for. */
|
||||
aud?: string | string[];
|
||||
/** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. */
|
||||
exp?: number;
|
||||
// unused claims excluded
|
||||
}
|
||||
interface IdTokenClaims extends JwtClaims {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
try {
|
||||
return jwtDecode<IdTokenClaims>(token);
|
||||
} catch (error) {
|
||||
logger.error("Could not decode id_token", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate idToken
|
||||
* https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
* @param idToken - id token from token endpoint
|
||||
* @param issuer - issuer for the OP as found during discovery
|
||||
* @param clientId - this client's id as registered with the OP
|
||||
* @param nonce - nonce used in the authentication request
|
||||
* @throws when id token is invalid
|
||||
*/
|
||||
export const validateIdToken = (idToken: string | undefined, issuer: string, clientId: string, nonce: string): void => {
|
||||
try {
|
||||
if (!idToken) {
|
||||
throw new Error("No ID token");
|
||||
}
|
||||
const claims = decodeIdToken(idToken);
|
||||
|
||||
// The Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
|
||||
if (claims.iss !== issuer) {
|
||||
throw new Error("Invalid issuer");
|
||||
}
|
||||
/**
|
||||
* The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience.
|
||||
* The aud (audience) Claim MAY contain an array with more than one element.
|
||||
* The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
|
||||
* EW: Don't accept tokens with other untrusted audiences
|
||||
* */
|
||||
if (claims.aud !== clientId) {
|
||||
throw new Error("Invalid audience");
|
||||
}
|
||||
|
||||
/**
|
||||
* If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
|
||||
* to verify that it is the same value as the one that was sent in the Authentication Request.
|
||||
*/
|
||||
if (claims.nonce !== nonce) {
|
||||
throw new Error("Invalid nonce");
|
||||
}
|
||||
|
||||
/**
|
||||
* The current time MUST be before the time represented by the exp Claim.
|
||||
* exp is an epoch timestamp in seconds
|
||||
* */
|
||||
if (!claims.exp || Date.now() > claims.exp * 1000) {
|
||||
throw new Error("Invalid expiry");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Invalid ID token", error);
|
||||
throw new Error(OidcError.InvalidIdToken);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { RustCrypto } from "./rust-crypto";
|
||||
import { logger } from "../logger";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
import { ICryptoCallbacks } from "../crypto";
|
||||
@@ -32,6 +31,8 @@ import { ICryptoCallbacks } from "../crypto";
|
||||
* @param deviceId - The local user's Device ID.
|
||||
* @param secretStorage - Interface to server-side secret storage.
|
||||
* @param cryptoCallbacks - Crypto callbacks provided by the application
|
||||
* @param storePrefix - the prefix to use on the indexeddbs created by rust-crypto.
|
||||
* If unset, a memory store will be used.
|
||||
*/
|
||||
export async function initRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
@@ -39,6 +40,7 @@ export async function initRustCrypto(
|
||||
deviceId: string,
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
cryptoCallbacks: ICryptoCallbacks,
|
||||
storePrefix: string | null,
|
||||
): Promise<RustCrypto> {
|
||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
@@ -51,7 +53,12 @@ export async function initRustCrypto(
|
||||
logger.info("Init OlmMachine");
|
||||
|
||||
// TODO: use the pickle key for the passphrase
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
|
||||
u,
|
||||
d,
|
||||
storePrefix ?? undefined,
|
||||
(storePrefix && "test pass") ?? undefined,
|
||||
);
|
||||
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
|
||||
+176
-30
@@ -19,7 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
@@ -32,15 +32,15 @@ import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import {
|
||||
BootstrapCrossSigningOpts,
|
||||
CreateSecretStorageOpts,
|
||||
CrossSigningKey,
|
||||
CrossSigningStatus,
|
||||
CryptoCallbacks,
|
||||
DeviceVerificationStatus,
|
||||
GeneratedSecretStorageKey,
|
||||
ImportRoomKeyProgressData,
|
||||
ImportRoomKeysOpts,
|
||||
VerificationRequest,
|
||||
CreateSecretStorageOpts,
|
||||
CryptoCallbacks,
|
||||
} from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
@@ -51,11 +51,15 @@ 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 } from "../@types/event";
|
||||
import { CryptoEvent } from "../crypto";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
*/
|
||||
export class RustCrypto implements CryptoBackend {
|
||||
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
|
||||
public globalErrorOnUnknownDevices = false;
|
||||
private _trustCrossSignedDevices = true;
|
||||
|
||||
@@ -96,6 +100,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
/** Crypto callbacks provided by the application */
|
||||
private readonly cryptoCallbacks: CryptoCallbacks,
|
||||
) {
|
||||
super();
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
this.eventDecryptor = new EventDecryptor(olmMachine);
|
||||
@@ -387,42 +392,96 @@ export class RustCrypto implements CryptoBackend {
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage,
|
||||
}: CreateSecretStorageOpts = {}): Promise<void> {
|
||||
// If createSecretStorageKey is not set, we stop
|
||||
if (!createSecretStorageKey) return;
|
||||
// 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
|
||||
const isNewSecretStorageKeyNeeded = setupNewSecretStorage || !(await this.secretStorageHasAESKey());
|
||||
|
||||
// See if we already have an AES secret-storage key.
|
||||
const secretStorageKeyTuple = await this.secretStorage.getKey();
|
||||
|
||||
if (secretStorageKeyTuple) {
|
||||
const [, keyInfo] = secretStorageKeyTuple;
|
||||
|
||||
// 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
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES && !setupNewSecretStorage) {
|
||||
return;
|
||||
if (isNewSecretStorageKeyNeeded) {
|
||||
if (!createSecretStorageKey) {
|
||||
throw new Error("unable to create a new secret storage key, createSecretStorageKey is not set");
|
||||
}
|
||||
|
||||
// Create a new storage key and add it to secret storage
|
||||
const recoveryKey = await createSecretStorageKey();
|
||||
await this.addSecretStorageKeyToSecretStorage(recoveryKey);
|
||||
}
|
||||
|
||||
const recoveryKey = await createSecretStorageKey();
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus();
|
||||
const hasPrivateKeys =
|
||||
crossSigningStatus.hasMaster && crossSigningStatus.hasSelfSigning && crossSigningStatus.hasUserSigning;
|
||||
|
||||
// If we have cross-signing private keys cached, store them in secret
|
||||
// storage if they are not there already.
|
||||
if (
|
||||
hasPrivateKeys &&
|
||||
(isNewSecretStorageKeyNeeded || !(await secretStorageContainsCrossSigningKeys(this.secretStorage)))
|
||||
) {
|
||||
const crossSigningPrivateKeys: RustSdkCryptoJs.CrossSigningKeyExport =
|
||||
await this.olmMachine.exportCrossSigningKeys();
|
||||
|
||||
if (!crossSigningPrivateKeys.masterKey) {
|
||||
throw new Error("missing master key in cross signing private keys");
|
||||
}
|
||||
|
||||
if (!crossSigningPrivateKeys.userSigningKey) {
|
||||
throw new Error("missing user signing key in cross signing private keys");
|
||||
}
|
||||
|
||||
if (!crossSigningPrivateKeys.self_signing_key) {
|
||||
throw new Error("missing self signing key in cross signing private keys");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the secretStorage key to the secret storage
|
||||
* - The secret storage key must have the `keyInfo` field filled
|
||||
* - The secret storage key is set as the default key of the secret storage
|
||||
* - Call `cryptoCallbacks.cacheSecretStorageKey` when done
|
||||
*
|
||||
* @param secretStorageKey - The secret storage key to add in the secret storage.
|
||||
*/
|
||||
private async addSecretStorageKeyToSecretStorage(secretStorageKey: GeneratedSecretStorageKey): Promise<void> {
|
||||
// keyInfo is required to continue
|
||||
if (!recoveryKey.keyInfo) {
|
||||
throw new Error("missing keyInfo field in the secret storage key created by createSecretStorageKey");
|
||||
if (!secretStorageKey.keyInfo) {
|
||||
throw new Error("missing keyInfo field in the secret storage key");
|
||||
}
|
||||
|
||||
const secretStorageKeyObject = await this.secretStorage.addKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
recoveryKey.keyInfo,
|
||||
secretStorageKey.keyInfo,
|
||||
);
|
||||
|
||||
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);
|
||||
|
||||
this.cryptoCallbacks.cacheSecretStorageKey?.(
|
||||
secretStorageKeyObject.keyId,
|
||||
secretStorageKeyObject.keyInfo,
|
||||
recoveryKey.privateKey,
|
||||
secretStorageKey.privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a secret storage AES Key is already added in secret storage
|
||||
*
|
||||
* @returns True if an AES key is in the secret storage
|
||||
*/
|
||||
private async secretStorageHasAESKey(): Promise<boolean> {
|
||||
// See if we already have an AES secret-storage key.
|
||||
const secretStorageKeyTuple = await this.secretStorage.getKey();
|
||||
|
||||
if (!secretStorageKeyTuple) return false;
|
||||
|
||||
const [, keyInfo] = secretStorageKeyTuple;
|
||||
|
||||
// Check if the key is an AES key
|
||||
return keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||
*/
|
||||
@@ -489,8 +548,19 @@ export class RustCrypto implements CryptoBackend {
|
||||
* @returns the VerificationRequests that are in progress
|
||||
*/
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
|
||||
// TODO
|
||||
return [];
|
||||
const requests: RustSdkCryptoJs.VerificationRequest[] = this.olmMachine.getVerificationRequests(
|
||||
new RustSdkCryptoJs.UserId(this.userId),
|
||||
);
|
||||
return requests
|
||||
.filter((request) => request.roomId === undefined)
|
||||
.map(
|
||||
(request) =>
|
||||
new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this.supportedVerificationMethods,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,6 +578,13 @@ export class RustCrypto implements CryptoBackend {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The verification methods we offer to the other side during an interactive verification.
|
||||
*
|
||||
* If `undefined`, we will offer all the methods supported by the Rust SDK.
|
||||
*/
|
||||
public supportedVerificationMethods: string[] | undefined;
|
||||
|
||||
/**
|
||||
* Send a verification request to our other devices.
|
||||
*
|
||||
@@ -517,8 +594,20 @@ export class RustCrypto implements CryptoBackend {
|
||||
*
|
||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||
*/
|
||||
public requestOwnUserVerification(): Promise<VerificationRequest> {
|
||||
throw new Error("not implemented");
|
||||
public async requestOwnUserVerification(): Promise<VerificationRequest> {
|
||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
|
||||
new RustSdkCryptoJs.UserId(this.userId),
|
||||
);
|
||||
if (userIdentity === undefined) {
|
||||
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
|
||||
}
|
||||
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await userIdentity.requestVerification(
|
||||
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,15 +615,29 @@ export class RustCrypto implements CryptoBackend {
|
||||
*
|
||||
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#requestDeviceVerification }.
|
||||
* Implementation of {@link CryptoApi#requestDeviceVerification}.
|
||||
*
|
||||
* @param userId - ID of the owner of the device to verify
|
||||
* @param deviceId - ID of the device to verify
|
||||
*
|
||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||
*/
|
||||
public requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
||||
throw new Error("not implemented");
|
||||
public async requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
||||
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
new RustSdkCryptoJs.DeviceId(deviceId),
|
||||
);
|
||||
|
||||
if (!device) {
|
||||
throw new Error("Not a known device");
|
||||
}
|
||||
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await device.requestVerification(
|
||||
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -578,10 +681,18 @@ export class RustCrypto implements CryptoBackend {
|
||||
* @param events - the received to-device messages
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
public preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||
public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
|
||||
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
||||
// one-time-keys, or fallback keys, so just pass empty data.
|
||||
return this.receiveSyncChanges({ events });
|
||||
const processed = await this.receiveSyncChanges({ events });
|
||||
|
||||
// look for interesting to-device messages
|
||||
for (const message of processed) {
|
||||
if (message.type === EventType.KeyVerificationRequest) {
|
||||
this.onIncomingKeyVerificationRequest(message.sender, message.content);
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
/** called by the sync loop to process one time key counts and unused fallback keys
|
||||
@@ -659,6 +770,32 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.outgoingRequestLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming m.key.verification request event
|
||||
*
|
||||
* @param sender - the sender of the event
|
||||
* @param content - the content of the event
|
||||
*/
|
||||
private onIncomingKeyVerificationRequest(sender: string, content: IContent): void {
|
||||
const transactionId = content.transaction_id;
|
||||
if (!transactionId || !sender) {
|
||||
// not a valid request: ignore
|
||||
return;
|
||||
}
|
||||
|
||||
const request: RustSdkCryptoJs.VerificationRequest | undefined = this.olmMachine.getVerificationRequest(
|
||||
new RustSdkCryptoJs.UserId(sender),
|
||||
transactionId,
|
||||
);
|
||||
|
||||
if (request) {
|
||||
this.emit(
|
||||
CryptoEvent.VerificationRequestReceived,
|
||||
new RustVerificationRequest(request, this.outgoingRequestProcessor, this.supportedVerificationMethods),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Other public functions
|
||||
@@ -847,3 +984,12 @@ class EventDecryptor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived;
|
||||
|
||||
type RustCryptoEventMap = {
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
*/
|
||||
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Emoji } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import {
|
||||
ShowQrCodeCallbacks,
|
||||
ShowSasCallbacks,
|
||||
VerificationPhase,
|
||||
VerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
VerificationRequestEventHandlerMap,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
VerifierEventHandlerMap,
|
||||
} from "../crypto-api/verification";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
|
||||
/**
|
||||
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
|
||||
*/
|
||||
export class RustVerificationRequest
|
||||
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
|
||||
implements VerificationRequest
|
||||
{
|
||||
/** Are we in the process of sending an `m.key.verification.ready` event? */
|
||||
private _accepting = false;
|
||||
|
||||
/** Are we in the process of sending an `m.key.verification.cancellation` event? */
|
||||
private _cancelling = false;
|
||||
|
||||
private _verifier: Verifier | undefined;
|
||||
|
||||
/**
|
||||
* Construct a new RustVerificationRequest to wrap the rust-level `VerificationRequest`.
|
||||
*
|
||||
* @param inner - VerificationRequest from the Rust SDK
|
||||
* @param outgoingRequestProcessor - `OutgoingRequestProcessor` to use for making outgoing HTTP requests
|
||||
* @param supportedVerificationMethods - Verification methods to use when `accept()` is called
|
||||
*/
|
||||
public constructor(
|
||||
private readonly inner: RustSdkCryptoJs.VerificationRequest,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
private readonly supportedVerificationMethods: string[] | undefined,
|
||||
) {
|
||||
super();
|
||||
|
||||
const onChange = async (): Promise<void> => {
|
||||
// if we now have a `Verification` where we lacked one before, wrap it.
|
||||
// TODO: QR support
|
||||
if (this._verifier === undefined) {
|
||||
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||
this._verifier = new RustSASVerifier(verification, this, outgoingRequestProcessor);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
};
|
||||
inner.registerChangesCallback(onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique ID for this verification request.
|
||||
*
|
||||
* An ID isn't assigned until the first message is sent, so this may be `undefined` in the early phases.
|
||||
*/
|
||||
public get transactionId(): string | undefined {
|
||||
return this.inner.flowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an in-room verification, the ID of the room.
|
||||
*
|
||||
* For to-device verifications, `undefined`.
|
||||
*/
|
||||
public get roomId(): string | undefined {
|
||||
return this.inner.roomId?.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this request was initiated by the local client.
|
||||
*
|
||||
* For in-room verifications, the initiator is who sent the `m.key.verification.request` event.
|
||||
* For to-device verifications, the initiator is who sent the `m.key.verification.start` event.
|
||||
*/
|
||||
public get initiatedByMe(): boolean {
|
||||
return this.inner.weStarted();
|
||||
}
|
||||
|
||||
/** The user id of the other party in this request */
|
||||
public get otherUserId(): string {
|
||||
return this.inner.otherUserId.toString();
|
||||
}
|
||||
|
||||
/** For verifications via to-device messages: the ID of the other device. Otherwise, undefined. */
|
||||
public get otherDeviceId(): string | undefined {
|
||||
return this.inner.otherDeviceId?.toString();
|
||||
}
|
||||
|
||||
/** True if the other party in this request is one of this user's own devices. */
|
||||
public get isSelfVerification(): boolean {
|
||||
return this.inner.isSelfVerification();
|
||||
}
|
||||
|
||||
/** current phase of the request. */
|
||||
public get phase(): VerificationPhase {
|
||||
const phase = this.inner.phase();
|
||||
|
||||
switch (phase) {
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Created:
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Requested:
|
||||
return VerificationPhase.Requested;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Ready:
|
||||
// if we're still sending the `m.key.verification.ready`, that counts as "Requested" in the js-sdk's
|
||||
// parlance.
|
||||
return this._accepting ? VerificationPhase.Requested : VerificationPhase.Ready;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Transitioned:
|
||||
return VerificationPhase.Started;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Done:
|
||||
return VerificationPhase.Done;
|
||||
case RustSdkCryptoJs.VerificationRequestPhase.Cancelled:
|
||||
return VerificationPhase.Cancelled;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown verification phase ${phase}`);
|
||||
}
|
||||
|
||||
/** True if the request has sent its initial event and needs more events to complete
|
||||
* (ie it is in phase `Requested`, `Ready` or `Started`).
|
||||
*/
|
||||
public get pending(): boolean {
|
||||
if (this.inner.isPassive()) return false;
|
||||
const phase = this.phase;
|
||||
return phase !== VerificationPhase.Done && phase !== VerificationPhase.Cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if we have started the process of sending an `m.key.verification.ready` (but have not necessarily received
|
||||
* the remote echo which causes a transition to {@link VerificationPhase.Ready}.
|
||||
*/
|
||||
public get accepting(): boolean {
|
||||
return this._accepting;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if we have started the process of sending an `m.key.verification.cancel` (but have not necessarily received
|
||||
* the remote echo which causes a transition to {@link VerificationPhase.Cancelled}).
|
||||
*/
|
||||
public get declining(): boolean {
|
||||
return this._cancelling;
|
||||
}
|
||||
|
||||
/**
|
||||
* The remaining number of ms before the request will be automatically cancelled.
|
||||
*
|
||||
* `null` indicates that there is no timeout
|
||||
*/
|
||||
public get timeout(): number | null {
|
||||
return this.inner.timeRemainingMillis();
|
||||
}
|
||||
|
||||
/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
|
||||
public get methods(): string[] {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/** the method picked in the .start event */
|
||||
public get chosenMethod(): string | null {
|
||||
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||
// TODO: this isn't quite right. The existence of a Verification doesn't prove that we have .started.
|
||||
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||
return "m.sas.v1";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the other party supports a given verification method.
|
||||
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
|
||||
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
|
||||
* For methods that need to be supported by both ends, use the `methods` property.
|
||||
*
|
||||
* @param method - the method to check
|
||||
* @returns true if the other party said they supported the method
|
||||
*/
|
||||
public otherPartySupportsMethod(method: string): boolean {
|
||||
const theirMethods: RustSdkCryptoJs.VerificationMethod[] | undefined = this.inner.theirSupportedMethods;
|
||||
if (theirMethods === undefined) {
|
||||
// no message from the other side yet
|
||||
return false;
|
||||
}
|
||||
|
||||
const requiredMethod = verificationMethodsByIdentifier[method];
|
||||
return theirMethods.some((m) => m === requiredMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the request, sending a .ready event to the other party
|
||||
*
|
||||
* @returns Promise which resolves when the event has been sent.
|
||||
*/
|
||||
public async accept(): Promise<void> {
|
||||
if (this.inner.phase() !== RustSdkCryptoJs.VerificationRequestPhase.Requested || this._accepting) {
|
||||
throw new Error(`Cannot accept a verification request in phase ${this.phase}`);
|
||||
}
|
||||
|
||||
this._accepting = true;
|
||||
try {
|
||||
const req: undefined | OutgoingRequest =
|
||||
this.supportedVerificationMethods === undefined
|
||||
? this.inner.accept()
|
||||
: this.inner.acceptWithMethods(
|
||||
this.supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
} finally {
|
||||
this._accepting = false;
|
||||
}
|
||||
|
||||
// phase may have changed, so emit a 'change' event
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the request, sending a cancellation to the other party
|
||||
*
|
||||
* @param params - Details for the cancellation, including `reason` (defaults to "User declined"), and `code`
|
||||
* (defaults to `m.user`).
|
||||
*
|
||||
* @returns Promise which resolves when the event has been sent.
|
||||
*/
|
||||
public async cancel(params?: { reason?: string; code?: string }): Promise<void> {
|
||||
if (this._cancelling) {
|
||||
// already cancelling; do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancelling = true;
|
||||
try {
|
||||
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
} finally {
|
||||
this._cancelling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Verifier} to do this verification via a particular method.
|
||||
*
|
||||
* If a verifier has already been created for this request, returns that verifier.
|
||||
*
|
||||
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Verifier#verifier} on the
|
||||
* returned verifier.
|
||||
*
|
||||
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
* @param targetDevice - details of where to send the request to.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*/
|
||||
public beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
* Implementation of {@link Crypto.VerificationRequest#startVerification}.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
*/
|
||||
public async startVerification(method: string): Promise<Verifier> {
|
||||
if (method !== "m.sas.v1") {
|
||||
throw new Error(`Unsupported verification method ${method}`);
|
||||
}
|
||||
|
||||
const res:
|
||||
| [RustSdkCryptoJs.Sas, RustSdkCryptoJs.RoomMessageRequest | RustSdkCryptoJs.ToDeviceRequest]
|
||||
| undefined = await this.inner.startSas();
|
||||
|
||||
if (res) {
|
||||
const [, req] = res;
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
|
||||
// this should have triggered the onChange callback, and we should now have a verifier
|
||||
if (!this._verifier) {
|
||||
throw new Error("Still no verifier after startSas() call");
|
||||
}
|
||||
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
* Only defined when the `phase` is Started.
|
||||
*/
|
||||
public get verifier(): Verifier | undefined {
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
|
||||
*/
|
||||
public getQRCodeBytes(): Buffer | undefined {
|
||||
// TODO
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
|
||||
* this verification.
|
||||
*/
|
||||
public get cancellationCode(): string | null {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the user that cancelled the request.
|
||||
*
|
||||
* Only defined when phase is Cancelled
|
||||
*/
|
||||
public get cancellingUserId(): string | undefined {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> implements Verifier {
|
||||
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
|
||||
private readonly completionPromise: Promise<void>;
|
||||
|
||||
private callbacks: ShowSasCallbacks | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly inner: RustSdkCryptoJs.Sas,
|
||||
_verificationRequest: RustVerificationRequest,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.completionPromise = new Promise<void>((resolve, reject) => {
|
||||
const onChange = async (): Promise<void> => {
|
||||
this.updateCallbacks();
|
||||
|
||||
if (this.inner.isDone()) {
|
||||
resolve(undefined);
|
||||
} else if (this.inner.isCancelled()) {
|
||||
const cancelInfo = this.inner.cancelInfo()!;
|
||||
reject(
|
||||
new Error(
|
||||
`Verification cancelled by ${
|
||||
cancelInfo.cancelledbyUs() ? "us" : "them"
|
||||
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
inner.registerChangesCallback(onChange);
|
||||
});
|
||||
// stop the runtime complaining if nobody catches a failure
|
||||
this.completionPromise.catch(() => null);
|
||||
}
|
||||
|
||||
/** if we can now show the callbacks, do so */
|
||||
private updateCallbacks(): void {
|
||||
if (this.callbacks === null) {
|
||||
const emoji: Array<Emoji> | undefined = this.inner.emoji();
|
||||
const decimal = this.inner.decimals() as [number, number, number] | undefined;
|
||||
|
||||
if (emoji === undefined && decimal === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callbacks = {
|
||||
sas: {
|
||||
decimal: decimal,
|
||||
emoji: emoji?.map((e) => [e.symbol, e.description]),
|
||||
},
|
||||
confirm: async (): Promise<void> => {
|
||||
const requests: Array<OutgoingRequest> = await this.inner.confirm();
|
||||
for (const m of requests) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(m);
|
||||
}
|
||||
},
|
||||
mismatch: (): void => {
|
||||
throw new Error("impl");
|
||||
},
|
||||
cancel: (): void => {
|
||||
throw new Error("impl");
|
||||
},
|
||||
};
|
||||
this.emit(VerifierEvent.ShowSas, this.callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the verification has been cancelled, either by us or the other side.
|
||||
*/
|
||||
public get hasBeenCancelled(): boolean {
|
||||
return this.inner.isCancelled();
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID of the other user in the verification process.
|
||||
*/
|
||||
public get userId(): string {
|
||||
return this.inner.otherUserId.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the key verification, if it has not already been started.
|
||||
*
|
||||
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
|
||||
* if the other side has already sent a start event.
|
||||
*
|
||||
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
|
||||
* or times out.
|
||||
*/
|
||||
public async verify(): Promise<void> {
|
||||
const req: undefined | OutgoingRequest = this.inner.accept();
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
await this.completionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a verification.
|
||||
*
|
||||
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
|
||||
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
|
||||
*
|
||||
* @param e - the reason for the cancellation.
|
||||
*/
|
||||
public cancel(e: Error): void {
|
||||
// TODO: something with `e`
|
||||
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||
if (req) {
|
||||
this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for an SAS verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
|
||||
* the SAS matches.
|
||||
*/
|
||||
public getShowSasCallbacks(): ShowSasCallbacks | null {
|
||||
return this.callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for reciprocating QR code verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
|
||||
* already scanned our QR code), and we are waiting for the user to confirm.
|
||||
*/
|
||||
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
|
||||
const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.VerificationMethod> = {
|
||||
"m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1,
|
||||
"m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
|
||||
"m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
|
||||
"m.reciprocate.v1": RustSdkCryptoJs.VerificationMethod.ReciprocateV1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a specced verification method identifier into a rust-side `VerificationMethod`.
|
||||
*
|
||||
* @param method - specced method identifier, for example `m.sas.v1`.
|
||||
* @returns Rust-side `VerificationMethod` corresponding to `method`.
|
||||
* @throws An error if the method is unknown.
|
||||
*/
|
||||
export function verificationMethodIdentifierToMethod(method: string): RustSdkCryptoJs.VerificationMethod {
|
||||
const meth = verificationMethodsByIdentifier[method];
|
||||
if (meth === undefined) {
|
||||
throw new Error(`Unknown verification method ${method}`);
|
||||
}
|
||||
return meth;
|
||||
}
|
||||
@@ -1091,15 +1091,15 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.41.0":
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
|
||||
integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
|
||||
"@eslint/js@8.43.0":
|
||||
version "8.43.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.43.0.tgz#559ca3d9ddbd6bf907ad524320a0d14b85586af0"
|
||||
integrity sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.8":
|
||||
version "0.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
||||
integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==
|
||||
"@humanwhocodes/config-array@^0.11.10":
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
||||
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
@@ -1426,10 +1426,10 @@
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.10":
|
||||
version "0.1.0-alpha.10"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
||||
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.1.tgz#aed6b7a3942bf9fc46234b7a572b9471a95da15f"
|
||||
integrity sha512-HsOOi/EnBb0Z+bkIxPuQQLW/9+Kijqkjqbf8xZq0S/fcHQJpIKxDJoZQDrVNpnEEWiIrBBLzVYTszXhMg+PYNw==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
@@ -1852,14 +1852,14 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.45.0":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.11.tgz#8d466aa21abea4c3f37129997b198d141f09e76f"
|
||||
integrity sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz#81382d6ecb92b8dda70e91f9035611cb2fecd1c3"
|
||||
integrity sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/type-utils" "5.59.11"
|
||||
"@typescript-eslint/utils" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/type-utils" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
@@ -1868,119 +1868,119 @@
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.45.0":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.11.tgz#af7d4b7110e3068ce0b97550736de455e4250103"
|
||||
integrity sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.1.tgz#0f2f58209c0862a73e3d5a56099abfdfa21d0fd3"
|
||||
integrity sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.11.tgz#5d131a67a19189c42598af9fb2ea1165252001ce"
|
||||
integrity sha512-dHFOsxoLFtrIcSj5h0QoBT/89hxQONwmn3FOQ0GOQcLOOXm+MIrS8zEAhs4tWl5MraxCY3ZJpaXQQdFMc2Tu+Q==
|
||||
"@typescript-eslint/scope-manager@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c"
|
||||
integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/visitor-keys" "5.59.11"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz#d43a3687aa4433868527cfe797eb267c6be35f19"
|
||||
integrity sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==
|
||||
"@typescript-eslint/scope-manager@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz#35abdb47f500c68c08f2f2b4f22c7c79472854bb"
|
||||
integrity sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/visitor-keys" "5.59.6"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
|
||||
"@typescript-eslint/type-utils@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.11.tgz#5eb67121808a84cb57d65a15f48f5bdda25f2346"
|
||||
integrity sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g==
|
||||
"@typescript-eslint/type-utils@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz#17770540e98d65ab4730c7aac618003f702893f4"
|
||||
integrity sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/utils" "5.59.11"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.11.tgz#1a9018fe3c565ba6969561f2a49f330cf1fe8db1"
|
||||
integrity sha512-epoN6R6tkvBYSc+cllrz+c2sOFWkbisJZWkOE+y3xHtvYaOE6Wk6B8e114McRJwFRjGvYdJwLXQH5c9osME/AA==
|
||||
"@typescript-eslint/types@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558"
|
||||
integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==
|
||||
|
||||
"@typescript-eslint/types@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b"
|
||||
integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==
|
||||
"@typescript-eslint/types@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.1.tgz#a17473910f6b8d388ea83c9d7051af89c4eb7561"
|
||||
integrity sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.11.tgz#b2caaa31725e17c33970c1197bcd54e3c5f42b9f"
|
||||
integrity sha512-YupOpot5hJO0maupJXixi6l5ETdrITxeo5eBOeuV7RSKgYdU3G5cxO49/9WRnJq9EMrB7AuTSLH/bqOsXi7wPA==
|
||||
"@typescript-eslint/typescript-estree@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
|
||||
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/visitor-keys" "5.59.11"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz#2fb80522687bd3825504925ea7e1b8de7bb6251b"
|
||||
integrity sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==
|
||||
"@typescript-eslint/typescript-estree@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz#8c71824b7165b64d5ebd7aa42968899525959834"
|
||||
integrity sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/visitor-keys" "5.59.6"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.11.tgz#9dbff49dc80bfdd9289f9f33548f2e8db3c59ba1"
|
||||
integrity sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg==
|
||||
"@typescript-eslint/utils@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.1.tgz#6861ebedbefba1ac85482d2bdef6f2ff1eb65b80"
|
||||
integrity sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.59.11"
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/typescript-estree" "5.59.11"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.6.tgz#82960fe23788113fc3b1f9d4663d6773b7907839"
|
||||
integrity sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c"
|
||||
integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.59.6"
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/typescript-estree" "5.59.6"
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.59.11":
|
||||
version "5.59.11"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.11.tgz#dca561ddad169dc27d62396d64f45b2d2c3ecc56"
|
||||
integrity sha512-KGYniTGG3AMTuKF9QBD7EIrvufkB6O6uX3knP73xbKLMpH+QRPcgnCxjWXSHjMRuOxFLovljqQgQpR0c7GvjoA==
|
||||
"@typescript-eslint/visitor-keys@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.0.tgz#b48b29da3f5f31dd1656281727004589d2722a66"
|
||||
integrity sha512-wm9Uz71SbCyhUKgcaPRauBdTegUyY/ZWl8gLwD/i/ybJqscrrdVSFImpvUz16BLPChIeKBK5Fa9s6KDQjsjyWw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.11"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz#673fccabf28943847d0c8e9e8d008e3ada7be6bb"
|
||||
integrity sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==
|
||||
"@typescript-eslint/visitor-keys@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz#19a877358bf96318ec35d90bfe6bd1445cce9434"
|
||||
integrity sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
JSONStream@^1.0.3:
|
||||
@@ -2055,11 +2055,16 @@ acorn@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.1.0, acorn@^8.4.1, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2:
|
||||
acorn@^8.1.0, acorn@^8.4.1, acorn@^8.8.0, acorn@^8.8.1:
|
||||
version "8.8.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||
|
||||
acorn@^8.8.2:
|
||||
version "8.9.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
|
||||
integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
@@ -3617,16 +3622,16 @@ eslint-plugin-import@^2.26.0:
|
||||
tsconfig-paths "^3.14.1"
|
||||
|
||||
eslint-plugin-jest@^27.1.6:
|
||||
version "27.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz#b85b4adf41c682ea29f1f01c8b11ccc39b5c672c"
|
||||
integrity sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==
|
||||
version "27.2.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.2.2.tgz#be4ded5f91905d9ec89aa8968d39c71f3b072c0c"
|
||||
integrity sha512-euzbp06F934Z7UDl5ZUaRPLAc9MKjh0rMPERrHT7UhlCEwgb25kBj37TvMgWeHZVkR5I9CayswrpoaqZU1RImw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-jsdoc@^46.0.0:
|
||||
version "46.2.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.2.6.tgz#f25590d371859f20691d65b5dcd4cbe370d65564"
|
||||
integrity sha512-zIaK3zbSrKuH12bP+SPybPgcHSM6MFzh3HFeaODzmsF1N8C1l8dzJ22cW1aq4g0+nayU1VMjmNf7hg0dpShLrA==
|
||||
version "46.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.3.0.tgz#9e19138b108f7289d5a5d5bacdfb75242f5ebcb7"
|
||||
integrity sha512-nfSvsR8YJRZyKrWwcXPSQyQC8jllfdEjcRhTXFr7RxfB5Wyl7AxrfjCUz72WwalkXMF4u+R6F/oDoW46ah69HQ==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.39.4"
|
||||
are-docs-informative "^0.0.2"
|
||||
@@ -3639,9 +3644,9 @@ eslint-plugin-jsdoc@^46.0.0:
|
||||
spdx-expression-parse "^3.0.1"
|
||||
|
||||
eslint-plugin-matrix-org@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-1.1.0.tgz#cb3c313b58aa84ee0dd52c57f4a614a1795e8744"
|
||||
integrity sha512-UArLqthBuaCljVajS2TtlPQLXNMZZAPKRt+gA8D0ayzcAj+Ghl50amwGtvLHMzISGv3sqNDBFBMD9cElntE1zA==
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-1.2.0.tgz#84b78969c93e6d3d593fe8bf25ee67ec4dcd2883"
|
||||
integrity sha512-Wp5CeLnyEwGBn8ZfVbSuO2y0Fs51IWonPJ1QRQTntaRxOkEQnnky3gOPwpfGJ8JB0CxYr1zXfeHh8LcYHW4wcg==
|
||||
|
||||
eslint-plugin-tsdoc@^0.2.17:
|
||||
version "0.2.17"
|
||||
@@ -3704,16 +3709,16 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
|
||||
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
|
||||
|
||||
eslint@8.41.0:
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
|
||||
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
|
||||
eslint@8.43.0:
|
||||
version "8.43.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.43.0.tgz#3e8c6066a57097adfd9d390b8fc93075f257a094"
|
||||
integrity sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@eslint/eslintrc" "^2.0.3"
|
||||
"@eslint/js" "8.41.0"
|
||||
"@humanwhocodes/config-array" "^0.11.8"
|
||||
"@eslint/js" "8.43.0"
|
||||
"@humanwhocodes/config-array" "^0.11.10"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
ajv "^6.10.0"
|
||||
@@ -5388,6 +5393,11 @@ jstransformer@1.0.0:
|
||||
is-promise "^2.0.0"
|
||||
promise "^7.0.1"
|
||||
|
||||
jwt-decode@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
|
||||
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
|
||||
|
||||
kind-of@^3.0.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||
@@ -5716,9 +5726,9 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56"
|
||||
integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.2.tgz#397e387fff22f6795844d00badc903a3d5de7057"
|
||||
integrity sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
@@ -6805,13 +6815,20 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1:
|
||||
semver@^7.3.5, semver@^7.3.8:
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
|
||||
integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.7, semver@^7.5.1:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
|
||||
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
sha.js@^2.4.0, sha.js@^2.4.8:
|
||||
version "2.4.11"
|
||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
||||
@@ -6852,9 +6869,9 @@ shell-quote@^1.6.1:
|
||||
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
|
||||
|
||||
shiki@^0.14.1:
|
||||
version "0.14.2"
|
||||
resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.2.tgz#d51440800b701392b31ce2336036058e338247a1"
|
||||
integrity sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==
|
||||
version "0.14.3"
|
||||
resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.3.tgz#d1a93c463942bdafb9866d74d619a4347d0bbf64"
|
||||
integrity sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==
|
||||
dependencies:
|
||||
ansi-sequence-parser "^1.1.0"
|
||||
jsonc-parser "^3.2.0"
|
||||
@@ -7197,9 +7214,9 @@ tapable@^2.2.0:
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
||||
terser@^5.5.1:
|
||||
version "5.17.7"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.7.tgz#2a8b134826fe179b711969fd9d9a0c2479b2a8c3"
|
||||
integrity sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==
|
||||
version "5.18.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.18.1.tgz#6d8642508ae9fb7b48768e48f16d675c89a78460"
|
||||
integrity sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
@@ -7510,9 +7527,9 @@ typedoc-plugin-versions@^0.2.3:
|
||||
semver "^7.3.7"
|
||||
|
||||
typedoc@^0.24.0:
|
||||
version "0.24.7"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.7.tgz#7eeb272a1894b3789acc1a94b3f2ae8e7330ee39"
|
||||
integrity sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==
|
||||
version "0.24.8"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.8.tgz#cce9f47ba6a8d52389f5e583716a2b3b4335b63e"
|
||||
integrity sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==
|
||||
dependencies:
|
||||
lunr "^2.3.9"
|
||||
marked "^4.3.0"
|
||||
@@ -7530,9 +7547,9 @@ typescript@^4.5.4:
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
typescript@^5.0.0:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
|
||||
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826"
|
||||
integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==
|
||||
|
||||
typeson-registry@^1.0.0-alpha.20:
|
||||
version "1.0.0-alpha.39"
|
||||
|
||||
Reference in New Issue
Block a user