Compare commits

...

21 Commits

Author SHA1 Message Date
David Baker ca00094e67 Fix bug where switching media caused media in subsequent calls to fail (#3489) 2023-06-20 15:16:43 +00:00
Richard van der Hoff 9c6d5a6c55 Element-R: wait for OlmMachine on startup (#3487)
* Element-R: wait for OlmMachine on startup

Previously, if you called `CryptoApi.getUserDeviceInfo()` before the first
`/sync` request happened, it would return an empty list, which made a bunch of
the tests racy. Add a hack to get the OlmMachine to think about its device
lists during startup.

* add a test
2023-06-20 09:29:41 +00:00
RiotRobot b77fe465f7 Resetting package fields for development 2023-06-20 10:14:37 +01:00
RiotRobot 8d93f49443 Merge branch 'master' into develop 2023-06-20 10:14:32 +01:00
RiotRobot f03fbed668 v26.1.0 2023-06-20 10:12:27 +01:00
RiotRobot c84efb57ef Prepare changelog for v26.1.0 2023-06-20 10:12:24 +01:00
Florian Duros 49f11578f7 ElementR: Add CryptoApi#bootstrapSecretStorage (#3483)
* Add WIP bootstrapSecretStorage

* Add new test if `createSecretStorageKey` is not set

* Remove old comments

* Add docs for `crypto-api.bootstrapSecretStorage`

* Remove default parameter for `createSecretStorageKey`

* Move `bootstrapSecretStorage` next to `isSecretStorageReady`

* Deprecate `bootstrapSecretStorage` in `MatrixClient`

* Update documentations

* Raise error if missing `keyInfo`

* Update behavior around `setupNewSecretStorage`

* Move `ICreateSecretStorageOpts` to `rust-crypto`

* Move `ICryptoCallbacks` to `rust-crypto`

* Update `bootstrapSecretStorage` documentation

* Add partial `CryptoCallbacks` documentation

* Fix typo

* Review changes

* Review changes
2023-06-20 08:40:11 +00:00
Richard van der Hoff 8df4be0939 Fix order of things in crypto-api.ts (#3491)
* `CryptoApi` should be first
* `export *` should be last
* everything else in the middle
2023-06-20 08:12:33 +00:00
Richard van der Hoff 80cdbe1058 Element-R: implement userHasCrossSigningKeys (#3488) 2023-06-19 21:11:04 +00:00
Michael Telatynski 9c62d15447 Specify git tags for cypress workflow so updating tests is gated by renovate PRs (#3480) 2023-06-19 07:38:24 +00:00
renovate[bot] afc70528cc Update definitelyTyped (#3465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 02:35:02 +00:00
Kerry f938d10f7b remove polls from room state on redaction (#3475) 2023-06-15 22:12:03 +00:00
Richard van der Hoff 22f0b781ea Add new methods for verification to CryptoApi (#3474)
* Add accessors for verification requests to CryptoApi

Part of https://github.com/vector-im/crypto-internal/issues/97

* Add new methods for verification to `CryptoApi` and deprecate old method

https://github.com/vector-im/crypto-internal/issues/98
2023-06-15 14:56:50 +01:00
Florian Duros 1bae10c4b2 Fix export type GeneratedSecretStorageKey (#3479) 2023-06-15 12:46:42 +00:00
Michael Telatynski 9b5b533c6f Close IDB database before deleting it to prevent spurious unexpected close errors (#3478) 2023-06-15 12:25:29 +00:00
Richard van der Hoff c425945353 Avoid deprecated classes in verification integ test (#3473)
https://github.com/matrix-org/matrix-js-sdk/pull/3449 deprecated a bunch of
exports from `src/crypto/verification/request/VerificationRequest`. Let's stop
using them in the integration test.
2023-06-15 11:24:01 +00:00
Florian Duros 0545f6df09 ElementR: Add rust-crypto#createRecoveryKeyFromPassphrase implementation (#3472)
* Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation

* Use `crypto`

* Rename `IRecoveryKey` into `GeneratedSecretStorageKey` for rust crypto

* Improve comments

* Improve `createRecoveryKeyFromPassphrase`
2023-06-14 14:38:43 +00:00
renovate[bot] d14fc426e6 Update dependency eslint-plugin-jsdoc to v46 (#3468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-14 10:49:28 +00:00
Michael Cousins 9f30defcd1 Upgrade JS-DevTools/npm-publish to v2 (#3456)
- Upgrade JS-DevTools/npm-publish to v2.2.0
- Remove workaround for bug JS-DevTools/npm-publish#15
- Remove usage of `jq` in favor of npm-publish output

Signed-off-by: Michael Cousins <michael@cousins.io>
2023-06-14 08:02:48 +00:00
renovate[bot] 9b175a4985 Update typescript-eslint monorepo to v5.59.9 (#3466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-13 20:52:40 +00:00
Richard van der Hoff 826ea5bc58 Pull out a new VerificationRequest interface (#3449)
* add a test for incoming verification requests

* Move `VerificationRequestEvent` to crypto-api

* Move `VerificationPhase` to `crypto-api`

* Define `VerificationRequest` interface

* Implement `canAcceptVerificationRequest`
2023-06-13 11:55:37 +00:00
25 changed files with 1073 additions and 257 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ module.exports = {
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},
+1 -1
View File
@@ -15,7 +15,7 @@ concurrency:
jobs:
cypress:
name: Cypress
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@develop
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.73.1
permissions:
actions: read
issues: read
+1 -1
View File
@@ -19,7 +19,7 @@ concurrency:
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@develop
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.73.1
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+6 -8
View File
@@ -24,18 +24,16 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
uses: JS-DevTools/npm-publish@a25b4180b728b0279fca97d4e5bccf391685aead # v2.2.0
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
ignore-scripts: false
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false
run: |
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
run: npm dist-tag add "$release" latest
env:
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
+2 -2
View File
@@ -1,5 +1,5 @@
Changes in [26.1.0-rc1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0-rc1) (2023-06-13)
==========================================================================================================
Changes in [26.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0) (2023-06-20)
==================================================================================================
## 🦖 Deprecations
* Introduce a new `Crypto.Verifier` interface, and deprecate direct access to `VerificationBase`, `SAS` and `ReciprocateQRCode` ([\#3414](https://github.com/matrix-org/matrix-js-sdk/pull/3414)).
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "26.1.0-rc1",
"version": "26.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -32,8 +32,8 @@
"keywords": [
"matrix-org"
],
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"main": "./src/index.ts",
"browser": "./src/browser-index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
@@ -107,7 +107,7 @@
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsdoc": "^46.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^47.0.0",
@@ -156,6 +156,5 @@
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
}
+161
View File
@@ -51,6 +51,7 @@ 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";
const ROOM_ID = "!room:id";
@@ -402,6 +403,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// 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.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
const Olm = global.Olm;
@@ -2169,4 +2171,163 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
});
describe("bootstrapSecretStorage", () => {
/**
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
*/
const createSecretStorageKey = jest.fn().mockResolvedValue({
keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
});
/**
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
* 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> {
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",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
if (content.key) {
resolve(content.key);
}
return {};
},
{ overwriteRoutes: true },
);
});
}
/**
* 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
* https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3sync
* @param secretStorageKey
*/
function sendSyncResponse(secretStorageKey: string) {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.default_key",
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
// Needed for secretStorage.getKey or secretStorage.hasKey
{
type: `m.secret_storage.key.${secretStorageKey}`,
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
}
beforeEach(async () => {
createSecretStorageKey.mockClear();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
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 create a new key", async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Finally, wait for bootstrapSecretStorage to finished
await bootstrapPromise;
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
// Check that the uploaded key in stored in the secret storage
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
// Check that the uploaded key is the default key
expect(defaultKeyId).toBe(secretStorageKey);
});
newBackendOnly(
"should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set",
async () => {
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Call again bootstrapSecretStorage
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
},
);
newBackendOnly(
"should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage",
async () => {
let bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
let secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Call again bootstrapSecretStorage
bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
},
);
});
});
+64 -17
View File
@@ -17,10 +17,19 @@ limitations under the License.
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
import { createClient, MatrixClient } from "../../../src";
import { ShowQrCodeCallbacks, ShowSasCallbacks, Verifier, VerifierEvent } from "../../../src/crypto-api/verification";
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
import {
canAcceptVerificationRequest,
ShowQrCodeCallbacks,
ShowSasCallbacks,
VerificationPhase,
VerificationRequest,
VerificationRequestEvent,
Verifier,
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, emitPromise, InitCrypto } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
@@ -31,11 +40,6 @@ import {
TEST_USER_ID,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import {
Phase,
VerificationRequest,
VerificationRequestEvent,
} from "../../../src/crypto/verification/request/VerificationRequest";
// 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.
@@ -126,11 +130,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// 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.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
expect(transactionId).toBeDefined();
expect(request.phase).toEqual(Phase.Requested);
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
@@ -148,7 +152,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(Phase.Ready);
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);
// ... and picks a method with m.key.verification.start
@@ -165,7 +169,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(Phase.Started);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.sas.v1");
// there should now be a verifier
@@ -238,7 +242,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(Phase.Done);
expect(request.phase).toEqual(VerificationPhase.Done);
// we're done with the temporary keypair
olmSAS.free();
@@ -269,7 +273,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// 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.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
@@ -290,7 +294,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(Phase.Ready);
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = request.getQRCodeBytes()!;
@@ -320,7 +324,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(Phase.Started);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
// there should now be a verifier
@@ -346,10 +350,53 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(Phase.Done);
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);
});
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
+18
View File
@@ -55,6 +55,7 @@ import * as threadUtils from "../test-utils/thread";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
import { logger } from "../../src/logger";
import { IMessageOpts } from "../test-utils/test-utils";
import { flushPromises } from "../test-utils/flushPromises";
describe("Room", function () {
const roomId = "!foo:bar";
@@ -3388,6 +3389,23 @@ describe("Room", function () {
const poll = room.polls.get(pollStartEventId);
expect(poll?.pollId).toBe(pollStartEventId);
});
it("removes poll from state when redacted", async () => {
const pollStartEvent = makePollStart("1");
const events = [pollStartEvent];
await room.processPollEvents(events);
expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy();
const redactedEvent = new MatrixEvent({ type: "m.room.redaction" });
pollStartEvent.makeRedacted(redactedEvent);
await flushPromises();
// removed from poll state
expect(room.polls.get(pollStartEvent.getId()!)).toBeFalsy();
});
});
describe("findPredecessorRoomId", () => {
+110 -4
View File
@@ -19,16 +19,26 @@ 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";
import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import {
HttpApiEvent,
HttpApiEventHandlerMap,
IHttpOpts,
IToDeviceEvent,
MatrixClient,
MatrixHttpApi,
TypedEventEmitter,
} from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { ImportRoomKeysOpts } from "../../../src/crypto-api";
import { CryptoCallbacks, ImportRoomKeysOpts } 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
@@ -211,6 +221,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});
@@ -333,6 +344,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
@@ -356,6 +368,99 @@ describe("RustCrypto", () => {
expect(res).toBe(null);
});
});
describe("userHasCrossSigningKeys", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto(undefined, testData.TEST_USER_ID);
});
it("returns false if there is no cross-signing identity", async () => {
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(false);
});
it("returns true if OlmMachine has a 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 cross-signing keys
const req = outgoingRequests.find((r) => r instanceof KeysQueryRequest)!;
await olmMachine.markRequestAsSent(
req.id!,
req.type,
JSON.stringify(testData.SIGNED_CROSS_SIGNING_KEYS_DATA),
);
// ... and we should now have cross-signing keys.
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
});
});
describe("createRecoveryKeyFromPassphrase", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto();
});
it("should create a recovery key without password", async () => {
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase();
// Expected the encoded private key to have 59 chars
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
// Expect the private key to be an Uint8Array with a length of 32
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
expect(recoveryKey.privateKey.length).toBe(32);
// Expect keyInfo to be empty
expect(Object.keys(recoveryKey.keyInfo!).length).toBe(0);
});
it("should create a recovery key with password", async () => {
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase("my password");
// Expected the encoded private key to have 59 chars
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
// Expect the private key to be an Uint8Array with a length of 32
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
expect(recoveryKey.privateKey.length).toBe(32);
// Expect keyInfo.passphrase to be filled
expect(recoveryKey.keyInfo?.passphrase?.algorithm).toBe("m.pbkdf2");
expect(recoveryKey.keyInfo?.passphrase?.iterations).toBe(500000);
});
});
it("should wait for a keys/query before returning devices", async () => {
jest.useFakeTimers();
const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
const rustCrypto = await makeTestRustCrypto(mockHttpApi, testData.TEST_USER_ID);
// an attempt to fetch the device list should block
const devicesPromise = rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
// ... until a /sync completes, and we trigger the outgoingRequests.
rustCrypto.onSyncCompleted({});
const deviceMap = (await devicesPromise).get(testData.TEST_USER_ID)!;
expect(deviceMap.has(TEST_DEVICE_ID)).toBe(true);
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
rustCrypto.stop();
});
});
/** build a basic RustCrypto instance for testing
@@ -367,6 +472,7 @@ async function makeTestRustCrypto(
userId: string = TEST_USER,
deviceId: string = TEST_DEVICE_ID,
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage);
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
}
+17 -2
View File
@@ -2223,7 +2223,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
// needed.
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
const rustCrypto = await RustCrypto.initRustCrypto(
this.http,
userId,
deviceId,
this.secretStorage,
this.cryptoCallbacks,
);
this.cryptoBackend = rustCrypto;
// attach the event listeners needed by RustCrypto
@@ -2431,12 +2437,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
* @deprecated Prefer {@link CryptoApi.findVerificationRequestDMInProgress}.
*/
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
} else if (!this.crypto) {
// Hack for element-R to avoid breaking the cypress tests. We can get rid of this once the react-sdk is
// updated to use CryptoApi.findVerificationRequestDMInProgress.
return undefined;
}
return this.cryptoBackend.findVerificationRequestDMInProgress(roomId);
return this.crypto.findVerificationRequestDMInProgress(roomId);
}
/**
@@ -2445,6 +2456,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param userId - the ID of the user to query
*
* @returns the VerificationRequests that are in progress
* @deprecated Prefer {@link CryptoApi.getVerificationRequestsToDeviceInProgress}.
*/
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
if (!this.crypto) {
@@ -2462,6 +2474,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns resolves to a VerificationRequest
* when the request has been sent to the other party.
*
* @deprecated Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}.
*/
public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
if (!this.crypto) {
@@ -2866,6 +2880,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* @deprecated Use {@link CryptoApi#bootstrapSecretStorage}.
*/
public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
if (!this.crypto) {
-10
View File
@@ -21,7 +21,6 @@ import { CryptoApi } from "../crypto-api";
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api";
import { IEventDecryptionResult } from "../@types/crypto";
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
/**
* Common interface for the crypto implementations
@@ -79,15 +78,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
*/
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
/**
* Finds a DM verification request that is already in progress for the given room id
*
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
*/
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
/**
* Get the cross signing information for a given user.
*
+161 -9
View File
@@ -18,13 +18,9 @@ import type { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
/** Types of cross-signing key */
export enum CrossSigningKey {
Master = "master",
SelfSigning = "self_signing",
UserSigning = "user_signing",
}
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { KeyBackupInfo } from "./crypto-api/keybackup";
/**
* Public interface to the cryptography parts of the js-sdk
@@ -195,12 +191,83 @@ export interface CryptoApi {
*/
isSecretStorageReady(): Promise<boolean>;
/**
* Bootstrap the secret storage by creating a new secret storage key and store it 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`.
* - Store this key in the secret storage and set it as the default key.
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
*
* @param opts - Options object.
*/
bootstrapSecretStorage(opts: CreateSecretStorageOpts): Promise<void>;
/**
* Get the status of our cross-signing keys.
*
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and whether the private keys are in secret storage.
*/
getCrossSigningStatus(): Promise<CrossSigningStatus>;
/**
* Create a recovery key (ie, a key suitable for use with server-side secret storage).
*
* The key can either be based on a user-supplied passphrase, or just created randomly.
*
* @param password - Optional passphrase string to use to derive the key,
* which can later be entered by the user as an alternative to entering the
* recovery key itself. If omitted, a key is generated randomly.
*
* @returns Object including recovery key and server upload parameters.
* The private key should be disposed of after displaying to the use.
*/
createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Device/User verification
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Returns to-device verification requests that are already in progress for the given user id.
*
* @param userId - the ID of the user to query
*
* @returns the VerificationRequests that are in progress
*/
getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[];
/**
* Finds a DM verification request that is already in progress for the given room id
*
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
*/
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
/**
* Send a verification request to our other devices.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
requestOwnUserVerification(): Promise<VerificationRequest>;
/**
* Request an interactive verification with the given device.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
*
* @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.
*/
requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest>;
}
/**
@@ -301,8 +368,6 @@ export interface ImportRoomKeysOpts {
source?: String; // TODO: Enum (backup, file, ??)
}
export * from "./crypto-api/verification";
/**
* The result of a call to {@link CryptoApi.getCrossSigningStatus}.
*/
@@ -324,3 +389,90 @@ export interface CrossSigningStatus {
userSigningKey: boolean;
};
}
/**
* Crypto callbacks provided by the application
*/
export interface CryptoCallbacks extends SecretStorageCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
/**
* Called by {@link CryptoApi#bootstrapSecretStorage}
* @param keyId - secret storage key id
* @param keyInfo - secret storage key info
* @param key - private key to store
*/
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
onSecretRequested?: (
userId: string,
deviceId: string,
requestId: string,
secretName: string,
deviceTrust: DeviceVerificationStatus,
) => Promise<string | undefined>;
getDehydrationKey?: (
keyInfo: SecretStorageKeyDescription,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
/**
* Parameter of {@link CryptoApi#bootstrapSecretStorage}
*/
export interface CreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: KeyBackupInfo;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
/** Types of cross-signing key */
export enum CrossSigningKey {
Master = "master",
SelfSigning = "self_signing",
UserSigning = "user_signing",
}
/**
* Recovery key created by {@link CryptoApi#createRecoveryKeyFromPassphrase}
*/
export interface GeneratedSecretStorageKey {
keyInfo?: AddSecretStorageKeyOpts;
/** The raw generated private key. */
privateKey: Uint8Array;
/** The generated key, encoded for display to the user per https://spec.matrix.org/v1.7/client-server-api/#key-representation. */
encodedPrivateKey?: string;
}
export * from "./crypto-api/verification";
export * from "./crypto-api/keybackup";
+42
View File
@@ -0,0 +1,42 @@
/*
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 { ISigned } from "../@types/signed";
export interface Curve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
export interface Aes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
/**
* Extra info of a recovery key
*/
export interface KeyBackupInfo {
algorithm: string;
auth_data: ISigned & (Curve25519AuthData | Aes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within
}
+189
View File
@@ -17,6 +17,187 @@ limitations under the License.
import { MatrixEvent } from "../models/event";
import { TypedEventEmitter } from "../models/typed-event-emitter";
/**
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
*/
export interface VerificationRequest
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap> {
/**
* 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.
*/
get transactionId(): string | undefined;
/**
* For an in-room verification, the ID of the room.
*
* For to-device verifictions, `undefined`.
*/
get roomId(): string | undefined;
/**
* 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.
*/
get initiatedByMe(): boolean;
/** The user id of the other party in this request */
get otherUserId(): string;
/** For verifications via to-device messages: the ID of the other device. Otherwise, undefined. */
get otherDeviceId(): string | undefined;
/** True if the other party in this request is one of this user's own devices. */
get isSelfVerification(): boolean;
/** current phase of the request. */
get phase(): VerificationPhase;
/** True if the request has sent its initial event and needs more events to complete
* (ie it is in phase `Requested`, `Ready` or `Started`).
*/
get pending(): boolean;
/**
* 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}.
*/
get accepting(): boolean;
/**
* 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}).
*/
get declining(): boolean;
/**
* The remaining number of ms before the request will be automatically cancelled.
*
* `null` indicates that there is no timeout
*/
get timeout(): number | null;
/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
get methods(): string[];
/** the method picked in the .start event */
get chosenMethod(): string | 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
*/
otherPartySupportsMethod(method: string): boolean;
/**
* Accepts the request, sending a .ready event to the other party
*
* @returns Promise which resolves when the event has been sent.
*/
accept(): Promise<void>;
/**
* 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.
*/
cancel(params?: { reason?: string; code?: string }): Promise<void>;
/**
* 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 Crypto.Verifier#verify} 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.
*/
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.
*/
get verifier(): Verifier | undefined;
/**
* 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.
*/
getQRCodeBytes(): Buffer | undefined;
/**
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
* this verification.
*/
get cancellationCode(): string | null;
/**
* The id of the user that cancelled the request.
*
* Only defined when phase is Cancelled
*/
get cancellingUserId(): string | undefined;
}
/** Events emitted by {@link VerificationRequest}. */
export enum VerificationRequestEvent {
/**
* Fires whenever the state of the request object has changed.
*
* There is no payload to the event.
*/
Change = "change",
}
/**
* Listener type map for {@link VerificationRequestEvent}s.
*
* @internal
*/
export type VerificationRequestEventHandlerMap = {
[VerificationRequestEvent.Change]: () => void;
};
/** The current phase of a verification request. */
export enum VerificationPhase {
/** Initial state: no event yet exchanged */
Unsent = 1,
/** An `m.key.verification.request` event has been sent or received */
Requested,
/** An `m.key.verification.ready` event has been sent or received, indicating the verification request is accepted. */
Ready,
/** An `m.key.verification.start` event has been sent or received, choosing a verification method */
Started,
/** An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling the verification request */
Cancelled,
/** An `m.key.verification.done` event has been **sent**, completing the verification request. */
Done,
}
/**
* A `Verifier` is responsible for performing the verification using a particular method, such as via QR code or SAS
* (emojis).
@@ -169,3 +350,11 @@ export interface GeneratedSas {
* English name.
*/
export type EmojiMapping = [emoji: string, name: string];
/**
* True if the request is in a state where it can be accepted (ie, that we're in phases {@link VerificationPhase.Unsent}
* or {@link VerificationPhase.Requested}, and that we're not in the process of sending a `ready` or `cancel`).
*/
export function canAcceptVerificationRequest(req: VerificationRequest): boolean {
return req.phase < VerificationPhase.Ready && !req.accepting && !req.declining;
}
+5 -43
View File
@@ -15,11 +15,14 @@ limitations under the License.
*/
import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupInfo } from "./keybackup";
import type { AddSecretStorageKeyOpts } from "../secret-storage";
/* re-exports for backwards compatibility. */
// CrossSigningKey is used as a value in `client.ts`, we can't export it as a type
export { CrossSigningKey } from "../crypto-api";
export type {
GeneratedSecretStorageKey as IRecoveryKey,
CreateSecretStorageOpts as ICreateSecretStorageOpts,
} from "../crypto-api";
export type {
ImportRoomKeyProgressData as IImportOpts,
@@ -65,44 +68,3 @@ export interface IEncryptedEventInfo {
*/
mismatchedSender: boolean;
}
export interface IRecoveryKey {
keyInfo?: AddSecretStorageKeyOpts;
privateKey: Uint8Array;
encodedPrivateKey?: string;
}
export interface ICreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<IRecoveryKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: IKeyBackupInfo;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
+13 -21
View File
@@ -80,7 +80,6 @@ import {
AccountDataClient,
AddSecretStorageKeyOpts,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageCallbacks,
SecretStorageKeyDescription,
SecretStorageKeyObject,
SecretStorageKeyTuple,
@@ -97,7 +96,10 @@ import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";
/* re-exports for backwards compatibility */
export type { BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts } from "../crypto-api";
export type {
BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts,
CryptoCallbacks as ICryptoCallbacks,
} from "../crypto-api";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -134,25 +136,6 @@ interface IInitOpts {
pickleKey?: string;
}
export interface ICryptoCallbacks extends SecretStorageCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
onSecretRequested?: (
userId: string,
deviceId: string,
requestId: string,
secretName: string,
deviceTrust: DeviceTrustLevel,
) => Promise<string | undefined>;
getDehydrationKey?: (
keyInfo: SecretStorageKeyDescription,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
/* eslint-disable camelcase */
interface IRoomKey {
room_id: string;
@@ -2356,6 +2339,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
}
/** @deprecated Use `requestOwnUserVerificationToDevice` or `requestDeviceVerification` */
public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
@@ -2368,6 +2352,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
}
public requestOwnUserVerification(): Promise<VerificationRequest> {
return this.requestVerification(this.userId);
}
public requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
return this.requestVerification(userId, [deviceId]);
}
private async requestVerificationWithChannel(
userId: string,
channel: IVerificationChannel,
+6 -21
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISigned } from "../@types/signed";
import { IEncryptedPayload } from "./aes";
export interface Curve25519SessionData {
@@ -35,27 +34,13 @@ export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
// Export for backward compatibility
export type {
Curve25519AuthData as ICurve25519AuthData,
Aes256AuthData as IAes256AuthData,
KeyBackupInfo as IKeyBackupInfo,
} from "../crypto-api/keybackup";
export interface IAes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
export interface IKeyBackupInfo {
algorithm: string;
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within
}
/* eslint-enable camelcase */
export interface IKeyBackupPrepareOpts {
@@ -24,6 +24,16 @@ import { EventType } from "../../../@types/event";
import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index";
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
import {
canAcceptVerificationRequest,
VerificationPhase as Phase,
VerificationRequest as IVerificationRequest,
VerificationRequestEvent,
VerificationRequestEventHandlerMap,
} from "../../../crypto-api/verification";
// backwards-compatibility exports
export { VerificationPhase as Phase, VerificationRequestEvent } from "../../../crypto-api/verification";
// How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
@@ -44,15 +54,6 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
export const READY_TYPE = EVENT_PREFIX + "ready";
export enum Phase {
Unsent = 1,
Requested,
Ready,
Started,
Cancelled,
Done,
}
// Legacy export fields
export const PHASE_UNSENT = Phase.Unsent;
export const PHASE_REQUESTED = Phase.Requested;
@@ -71,26 +72,17 @@ interface ITransition {
event?: MatrixEvent;
}
export enum VerificationRequestEvent {
Change = "change",
}
type EventHandlerMap = {
/**
* Fires whenever the state of the request object has changed.
*/
[VerificationRequestEvent.Change]: () => void;
};
/**
* State machine for verification requests.
* Things that differ based on what channel is used to
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
*
* @deprecated Avoid direct references: instead prefer {@link Crypto.VerificationRequest}.
*/
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends TypedEventEmitter<
VerificationRequestEvent,
EventHandlerMap
> {
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel>
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
implements IVerificationRequest
{
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
@@ -257,7 +249,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
}
public get canAccept(): boolean {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
return canAcceptVerificationRequest(this);
}
public get accepting(): boolean {
+5
View File
@@ -2008,6 +2008,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const poll = new Poll(event, this.client, this);
this.polls.set(event.getId()!, poll);
this.emit(PollEvent.New, poll);
// remove the poll when redacted
event.once(MatrixEventEvent.BeforeRedaction, (redactedEvent: MatrixEvent) => {
this.polls.delete(redactedEvent.getId()!);
});
} catch {}
// poll creation can fail for malformed poll start events
return;
+15 -1
View File
@@ -21,6 +21,7 @@ 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";
/**
* Create a new `RustCrypto` implementation
@@ -30,12 +31,14 @@ import { ServerSideSecretStorage } from "../secret-storage";
* @param userId - The local user's User ID.
* @param deviceId - The local user's Device ID.
* @param secretStorage - Interface to server-side secret storage.
* @param cryptoCallbacks - Crypto callbacks provided by the application
*/
export async function initRustCrypto(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
userId: string,
deviceId: string,
secretStorage: ServerSideSecretStorage,
cryptoCallbacks: ICryptoCallbacks,
): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
await RustSdkCryptoJs.initAsync();
@@ -49,11 +52,22 @@ export async function initRustCrypto(
// TODO: use the pickle key for the passphrase
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage);
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions),
);
// Tell the OlmMachine to think about its outgoing requests before we hand control back to the application.
//
// This is primarily a fudge to get it to correctly populate the `users_for_key_query` list, so that future
// calls to getIdentity (etc) block until the key queries are performed.
//
// Note that we don't actually need to *make* any requests here; it is sufficient to tell the Rust side to think
// about them.
//
// XXX: find a less hacky way to do this.
await olmMachine.outgoingRequests();
logger.info("Completed rust crypto-sdk setup");
return rustCrypto;
}
+148 -16
View File
@@ -32,18 +32,25 @@ import { KeyClaimManager } from "./KeyClaimManager";
import { MapWithDefault } from "../utils";
import {
BootstrapCrossSigningOpts,
CrossSigningKey,
CrossSigningStatus,
DeviceVerificationStatus,
GeneratedSecretStorageKey,
ImportRoomKeyProgressData,
ImportRoomKeysOpts,
VerificationRequest,
CreateSecretStorageOpts,
CryptoCallbacks,
} from "../crypto-api";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device";
import { ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningKey } from "../crypto/api";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningIdentity } from "./CrossSigningIdentity";
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
@@ -85,6 +92,9 @@ export class RustCrypto implements CryptoBackend {
/** Interface to server-side secret storage */
private readonly secretStorage: ServerSideSecretStorage,
/** Crypto callbacks provided by the application */
private readonly cryptoCallbacks: CryptoCallbacks,
) {
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
@@ -161,18 +171,6 @@ export class RustCrypto implements CryptoBackend {
return new UserTrustLevel(false, false, false);
}
/**
* Finds a DM verification request that is already in progress for the given room id
*
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
*/
public findVerificationRequestDMInProgress(roomId: string): undefined {
// TODO
return;
}
/**
* Get the cross signing information for a given user.
*
@@ -195,9 +193,12 @@ export class RustCrypto implements CryptoBackend {
public globalBlacklistUnverifiedDevices = false;
/**
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
*/
public async userHasCrossSigningKeys(): Promise<boolean> {
// TODO
return false;
const userIdentity = await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
return userIdentity !== undefined;
}
public prepareToEncrypt(room: Room): void {
@@ -379,6 +380,49 @@ export class RustCrypto implements CryptoBackend {
return false;
}
/**
* Implementation of {@link CryptoApi#bootstrapSecretStorage}
*/
public async bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage,
}: CreateSecretStorageOpts = {}): Promise<void> {
// If createSecretStorageKey is not set, we stop
if (!createSecretStorageKey) return;
// 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;
}
}
const recoveryKey = await createSecretStorageKey();
// keyInfo is required to continue
if (!recoveryKey.keyInfo) {
throw new Error("missing keyInfo field in the secret storage key created by createSecretStorageKey");
}
const secretStorageKeyObject = await this.secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
recoveryKey.keyInfo,
);
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);
this.cryptoCallbacks.cacheSecretStorageKey?.(
secretStorageKeyObject.keyId,
secretStorageKeyObject.keyInfo,
recoveryKey.privateKey,
);
}
/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/
@@ -405,6 +449,94 @@ export class RustCrypto implements CryptoBackend {
};
}
/**
* Implementation of {@link CryptoApi#createRecoveryKeyFromPassphrase}
*/
public async createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey> {
let key: Uint8Array;
const keyInfo: AddSecretStorageKeyOpts = {};
if (password) {
// Generate the key from the passphrase
const derivation = await keyFromPassphrase(password);
keyInfo.passphrase = {
algorithm: "m.pbkdf2",
iterations: derivation.iterations,
salt: derivation.salt,
};
key = derivation.key;
} else {
// Using the navigator crypto API to generate the private key
key = new Uint8Array(32);
crypto.getRandomValues(key);
}
const encodedPrivateKey = encodeRecoveryKey(key);
return {
keyInfo,
encodedPrivateKey,
privateKey: key,
};
}
/**
* Returns to-device verification requests that are already in progress for the given user id.
*
* Implementation of {@link CryptoApi#getVerificationRequestsToDeviceInProgress}
*
* @param userId - the ID of the user to query
*
* @returns the VerificationRequests that are in progress
*/
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
// TODO
return [];
}
/**
* Finds a DM verification request that is already in progress for the given room id
*
* Implementation of {@link CryptoApi#findVerificationRequestDMInProgress}
*
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
*
*/
public findVerificationRequestDMInProgress(roomId: string): undefined {
// TODO
return;
}
/**
* Send a verification request to our other devices.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
*
* Implementation of {@link CryptoApi#requestOwnUserVerification}.
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
public requestOwnUserVerification(): Promise<VerificationRequest> {
throw new Error("not implemented");
}
/**
* Request an interactive verification with the given device.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
*
* 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");
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
+5 -1
View File
@@ -338,12 +338,16 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* to prevent mixing data between accounts. Closes the database.
* @returns Resolved when the database is cleared.
*/
public clearDatabase(): Promise<void> {
return new Promise((resolve) => {
logger.log(`Removing indexeddb instance: ${this.dbName}`);
// Close the database first to avoid firing unexpected close events
this.db?.close();
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = (): void => {
+12
View File
@@ -334,7 +334,19 @@ export class MediaHandler extends TypedEventEmitter<
this.emit(MediaHandlerEvent.LocalStreamsChanged);
if (this.localUserMediaStream === mediaStream) {
// if we have this stream cahced, remove it, because we've stopped it
this.localUserMediaStream = undefined;
} else {
// If it's not the same stream. remove any tracks from the cached stream that
// we have just stopped, and if we do stop any, call the same method on the
// cached stream too in order to stop all its tracks (in case they are different)
// and un-cache it.
for (const track of mediaStream.getTracks()) {
if (this.localUserMediaStream?.getTrackById(track.id)) {
this.stopUserMediaStream(this.localUserMediaStream);
break;
}
}
}
}
+69 -68
View File
@@ -1790,9 +1790,9 @@
integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
"@types/node@18":
version "18.16.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==
version "18.16.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.18.tgz#85da09bafb66d4bc14f7c899185336d0c1736390"
integrity sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@@ -1830,9 +1830,9 @@
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/uuid@9":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6"
integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==
version "9.0.2"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b"
integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==
"@types/webidl-conversions@*":
version "7.0.0"
@@ -1852,14 +1852,14 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.45.0":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz#1e7a3e5318ece22251dfbc5c9c6feeb4793cc509"
integrity sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==
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==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
"@typescript-eslint/scope-manager" "5.59.8"
"@typescript-eslint/type-utils" "5.59.8"
"@typescript-eslint/utils" "5.59.8"
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/type-utils" "5.59.11"
"@typescript-eslint/utils" "5.59.11"
debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
@@ -1868,15 +1868,23 @@
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.45.0":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.8.tgz#60cbb00671d86cf746044ab797900b1448188567"
integrity sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.11.tgz#af7d4b7110e3068ce0b97550736de455e4250103"
integrity sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==
dependencies:
"@typescript-eslint/scope-manager" "5.59.8"
"@typescript-eslint/types" "5.59.8"
"@typescript-eslint/typescript-estree" "5.59.8"
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/typescript-estree" "5.59.11"
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==
dependencies:
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/visitor-keys" "5.59.11"
"@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"
@@ -1885,33 +1893,38 @@
"@typescript-eslint/types" "5.59.6"
"@typescript-eslint/visitor-keys" "5.59.6"
"@typescript-eslint/scope-manager@5.59.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz#ff4ad4fec6433647b817c4a7d4b4165d18ea2fa8"
integrity sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==
"@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==
dependencies:
"@typescript-eslint/types" "5.59.8"
"@typescript-eslint/visitor-keys" "5.59.8"
"@typescript-eslint/type-utils@5.59.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz#aa6c029a9d7706d26bbd25eb4666398781df6ea2"
integrity sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==
dependencies:
"@typescript-eslint/typescript-estree" "5.59.8"
"@typescript-eslint/utils" "5.59.8"
"@typescript-eslint/typescript-estree" "5.59.11"
"@typescript-eslint/utils" "5.59.11"
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.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.59.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.8.tgz#212e54414733618f5d0fd50b2da2717f630aebf8"
integrity sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==
"@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==
dependencies:
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/visitor-keys" "5.59.11"
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"
@@ -1926,30 +1939,17 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.59.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz#801a7b1766481629481b3b0878148bd7a1f345d7"
integrity sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==
dependencies:
"@typescript-eslint/types" "5.59.8"
"@typescript-eslint/visitor-keys" "5.59.8"
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.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.8.tgz#34d129f35a2134c67fdaf024941e8f96050dca2b"
integrity sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==
"@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==
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.8"
"@typescript-eslint/types" "5.59.8"
"@typescript-eslint/typescript-estree" "5.59.8"
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/typescript-estree" "5.59.11"
eslint-scope "^5.1.1"
semver "^7.3.7"
@@ -1967,6 +1967,14 @@
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==
dependencies:
"@typescript-eslint/types" "5.59.11"
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"
@@ -1975,14 +1983,6 @@
"@typescript-eslint/types" "5.59.6"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.59.8":
version "5.59.8"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz#aa6a7ef862add919401470c09e1609392ef3cc40"
integrity sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==
dependencies:
"@typescript-eslint/types" "5.59.8"
eslint-visitor-keys "^3.3.0"
JSONStream@^1.0.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -3623,10 +3623,10 @@ eslint-plugin-jest@^27.1.6:
dependencies:
"@typescript-eslint/utils" "^5.10.0"
eslint-plugin-jsdoc@^45.0.0:
version "45.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-45.0.0.tgz#6be84e4842a7138cc571a907ea9c31c42eaac5c0"
integrity sha512-l2+Jcs/Ps7oFA+SWY+0sweU/e5LgricnEl6EsDlyRTF5y0+NWL1y9Qwz9PHwHAxtdJq6lxPjEQWmYLMkvhzD4g==
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==
dependencies:
"@es-joy/jsdoccomment" "~0.39.4"
are-docs-informative "^0.0.2"
@@ -3634,6 +3634,7 @@ eslint-plugin-jsdoc@^45.0.0:
debug "^4.3.4"
escape-string-regexp "^4.0.0"
esquery "^1.5.0"
is-builtin-module "^3.2.1"
semver "^7.5.1"
spdx-expression-parse "^3.0.1"