Compare commits

...

23 Commits

Author SHA1 Message Date
RiotRobot c9b43ab251 v34.8.0 2024-10-15 10:53:23 +00:00
David Baker 2fb1e659c8 Merge commit from fork
Remove insecure MatrixClient.sendSharedHistoryKeys method
2024-10-15 11:48:16 +01:00
RiotRobot 868bbfcb31 v34.7.0 2024-10-08 12:22:28 +00:00
RiotRobot c1b4f97372 v34.7.0-rc.0 2024-10-01 14:54:58 +00:00
Florian Duros 5f3b89990d Move out crypto/aes (#4431)
* Move `SecretEncryptedPayload` in `src/utils/@types`

* Move `encryptAES` to a dedicated file. Moved in a utils folder.

* Move `deriveKeys` to a dedicated file in order to share it

* Move `decryptAES` to a dedicated file. Moved in a utils folder.

* Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder.

* Remove AES functions in `aes.ts` and export new ones for backward compatibility

* Update import to use new functions

* Add `src/utils` entrypoint in `README.md`

* - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`.
- Move into `src/@types`

* Move `calculateKeyCheck` into `secret-storage.ts`.

* Move `deriveKeys` into `src/utils/internal` folder.

* - Rename `encryptAES` on `encryptAESSecretStorageItem`
- Change named export by default export

* - Rename `decryptAES` on `decryptAESSecretStorageItem`
- Change named export by default export

* Update documentation

* Update `decryptAESSecretStorageItem` doc

* Add lnk to spec for `calculateKeyCheck`

* Fix downstream tests
2024-10-01 13:52:59 +00:00
Richard van der Hoff 866fd6f4a3 Bump matrix-rust-sdk to 9.1.0 (#4435)
Mostly, to pick up https://github.com/matrix-org/matrix-rust-sdk/pull/3985
2024-09-30 16:46:33 +00:00
Valere 9ecb66e695 crypto: configure key sharing strategy based on DeviceIsolationMode (#4425)
* crypto: configure key sharing strategy based on deviceIsolationMode

fix eslint import error

cryptoMode was renamed to deviceIsolationMode

post rebase fix: Device Isolation mode name changes

* Fix outdated docs referring to old cryptomode

* code review: better comment for globalBlacklistUnverifiedDevices option

* RoomEncryptor: Use appropriate default for getBlacklistUnverifiedDevices

* do not provide a default value for DeviceIsolationMode for encryption

* Update src/rust-crypto/RoomEncryptor.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-30 14:26:34 +00:00
Hugh Nimmo-Smith baa6d13506 RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() (#4427)
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys()

* Clarify comment

* Feedback from code review

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

* Fix test

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-09-27 15:55:07 +00:00
Hugh Nimmo-Smith 2d6230f199 Rotate RTC key when a new member joins (#4422) 2024-09-27 15:54:48 +00:00
Tulir Asokan 825d85f18d Update media event content types to include captions (#4403)
Signed-off-by: Tulir Asokan <tulir@maunium.net>
2024-09-26 16:06:59 +00:00
Richard van der Hoff 1dcb7a6e75 Remove insecure MatrixClient.sendSharedHistoryKeys method
This method is impossible to use securely, and so is being removed. (It also
didn't work under Rust cryptography.)

In future, this functionality may be re-introduced in a safer way, but doing so
will probably require updates to the MSC.
2024-09-26 12:03:45 +01:00
Richard van der Hoff 823316b2ff Remove use of insecure sendSharedHistoryKeys in MSC3089 impl 2024-09-26 12:01:51 +01:00
renovate[bot] d56fa197d0 Update dependency typescript to v5.6.2 (#4420)
* Update dependency typescript to v5.6.2

* Fix TS errors

* Update minimal version of TS to `5.4.2` since the code is not compliant with an older version.

* Review fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2024-09-26 08:23:38 +00:00
Michael Telatynski f7229bfff0 Update OIDC registration types to match latest MSC2966 state (#4432)
* Update OIDC registration types to match latest MSC2966 state

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comment

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-09-25 16:30:13 +00:00
Valere 538717c23e crypto: Replace cryptoMode with DeviceIsolationMode concept (#4429)
* crypto: Replace cryptoMode with DeviceIsolationMode concept

* use enum instead of string for the IsolationMode kind

* Code review - Cleaning, renaming

* review: unneeded @see in doc

* review: Rename IsolationMode with better names

* review: quick cleaning and doc
2024-09-25 13:33:02 +00:00
Richard van der Hoff 1a8ea3d685 Add CryptoApi.pinCurrentUserIdentity and UserIdentity.needsUserApproval (#4415)
* Implement `UserVerificationStatus.needsUserApproval`

Expose the `identityNeedsUserApproval` flag from the rust crypto crate.

* Add CryptoApi.pinCurrentUserIdentity

Expose `pinCurrentMasterKey` from the rust crypto api.

* Test data: add second cross-signing key for Bob

* Add tests for verification status
2024-09-24 16:38:18 +00:00
Florian Duros d0890d9450 Update sonarcloud action to use v3.3 matrix-org/sonarcloud-workflow-action (#4430) 2024-09-24 15:35:40 +00:00
RiotRobot 42ec17b977 Merge branch 'master' into develop 2024-09-24 12:37:47 +00:00
RiotRobot 82e9eefce6 v34.6.0 2024-09-24 12:37:19 +00:00
renovate[bot] f8208b1891 Update typedoc (#4419)
* Update typedoc

* Don't link a private method in tsdoc of a public method

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
2024-09-23 13:47:49 +00:00
Richard van der Hoff 092a59af66 Throw clearer error if createSecretStorageKey misbehaves (#4412)
https://github.com/element-hq/element-web/issues/27888 shows a slightly
mysterious error message, but the problem is `createSecretStorageKey`
returning the wrong thing.
2024-09-23 10:25:42 +00:00
renovate[bot] dbd7d26968 Update dependency @types/node to v18.19.50 (#4418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 15:26:50 +00:00
renovate[bot] 4fda9e8419 Update all non-major dependencies (#4417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 13:38:54 +00:00
44 changed files with 1448 additions and 670 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v3.2
uses: matrix-org/sonarcloud-workflow-action@v3.3
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
+35
View File
@@ -1,3 +1,38 @@
Changes in [34.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.8.0) (2024-10-15)
==================================================================================================
This release removes insecure functionality, resolving CVE-2024-47080 / GHSA-4jf8-g8wp-cx7c.
Changes in [34.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.7.0) (2024-10-08)
==================================================================================================
## 🦖 Deprecations
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() ([#4427](https://github.com/matrix-org/matrix-js-sdk/pull/4427)). Contributed by @hughns.
## ✨ Features
* Bump matrix-rust-sdk to 9.1.0 ([#4435](https://github.com/matrix-org/matrix-js-sdk/pull/4435)). Contributed by @richvdh.
* Rotate Matrix RTC media encryption key when a new member joins a call for Post Compromise Security ([#4422](https://github.com/matrix-org/matrix-js-sdk/pull/4422)). Contributed by @hughns.
* Update media event content types to include captions ([#4403](https://github.com/matrix-org/matrix-js-sdk/pull/4403)). Contributed by @tulir.
* Update OIDC registration types to match latest MSC2966 state ([#4432](https://github.com/matrix-org/matrix-js-sdk/pull/4432)). Contributed by @t3chguy.
* Add `CryptoApi.pinCurrentUserIdentity` and `UserIdentity.needsUserApproval` ([#4415](https://github.com/matrix-org/matrix-js-sdk/pull/4415)). Contributed by @richvdh.
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
==================================================================================================
## 🦖 Deprecations
* Element-R: Mark unsupported MatrixClient methods as deprecated ([#4389](https://github.com/matrix-org/matrix-js-sdk/pull/4389)). Contributed by @richvdh.
## ✨ Features
* Add crypto mode setting for invisible crypto, and apply it to decrypting events ([#4407](https://github.com/matrix-org/matrix-js-sdk/pull/4407)). Contributed by @uhoreg.
* Don't share full key history for RTC per-participant encryption ([#4406](https://github.com/matrix-org/matrix-js-sdk/pull/4406)). Contributed by @hughns.
* Export membership types ([#4405](https://github.com/matrix-org/matrix-js-sdk/pull/4405)). Contributed by @Johennes.
* Fix sending redacts in embedded (widget) mode ([#4398](https://github.com/matrix-org/matrix-js-sdk/pull/4398)). Contributed by @toger5.
* Expose the event ID of a call membership ([#4395](https://github.com/matrix-org/matrix-js-sdk/pull/4395)). Contributed by @robintown.
* MSC4133 - Extended profiles ([#4391](https://github.com/matrix-org/matrix-js-sdk/pull/4391)). Contributed by @Half-Shot.
Changes in [34.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.5.0) (2024-09-10)
==================================================================================================
## 🦖 Deprecations
+1
View File
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
## Examples
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.6.0-rc.0",
"version": "34.8.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -122,7 +122,7 @@
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.3.3"
"typescript": "^5.4.2"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
+4 -4
View File
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
const masterKey = await encryptAESSecretStorageItem(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
const selfSigningKey = await encryptAESSecretStorageItem(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
const userSigningKey = await encryptAESSecretStorageItem(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
+94 -31
View File
@@ -82,11 +82,13 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import {
CrossSigningKey,
CryptoCallbacks,
CryptoMode,
DecryptionFailureCode,
DeviceIsolationMode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup";
@@ -747,9 +749,34 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
newBackendOnly(
"fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
describe("IsolationMode decryption tests", () => {
newBackendOnly(
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
},
);
newBackendOnly(
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
},
);
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
// This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto
// mode.
@@ -760,7 +787,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Invisible);
aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
await startClientAndAwaitFirstSync();
@@ -807,26 +834,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event);
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
// Next, try decrypting in transition mode, which should also
// fail for the same reason
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Transition);
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
// Decrypting in legacy mode should succeed since it doesn't
// care about device trust.
aliceClient.getCrypto()!.setCryptoMode(CryptoMode.Legacy);
await event.attemptDecryption(aliceClient["cryptoBackend"]!);
expect(decryptedEvent.decryptionFailureReason).toEqual(null);
},
);
return await testUtils.awaitDecryption(event);
}
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
@@ -3261,15 +3271,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
});
describe("Check if the cross signing keys are available for a user", () => {
describe("User identity", () => {
let keyResponder: E2EKeyResponder;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
@@ -3285,6 +3293,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.getCrypto()!
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are available for a tracked user", async () => {
@@ -3295,11 +3309,60 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are not available for an unknown user", async () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
newBackendOnly("An unverified user changes identity", async () => {
// We have to be tracking Bob's keys, which means we need to share a room with him
syncResponder.sendOrQueueSyncResponse({
...getSyncResponse([BOB_TEST_USER_ID]),
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
// Bob changes his cross-signing keys
keyResponder.addCrossSigningData(testData.BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA);
syncResponder.sendOrQueueSyncResponse({
next_batch: "2",
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(true);
}
// Pinning the new identity should clear the needsUserApproval flag.
await aliceClient.getCrypto()!.pinCurrentUserIdentity(BOB_TEST_USER_ID);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.needsUserApproval).toBe(false);
}
});
});
+15 -5
View File
@@ -101,9 +101,8 @@ export const makeGeolocationPosition = ({
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
}): GeolocationPosition => {
const { toJSON, ...coordsJSON } = {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
@@ -112,5 +111,16 @@ export const makeGeolocationPosition = ({
heading: null,
speed: null,
...coords,
},
});
};
const posJSON = {
timestamp: timestamp ?? 1647256791840,
coords: {
toJSON: () => coordsJSON,
...coordsJSON,
},
};
return {
toJSON: () => posJSON,
...posJSON,
};
};
@@ -66,6 +66,7 @@ BOB_DATA = {
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"DoYouSpeakWhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
result = f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
if alt_master_key is not None:
result += f"""
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
};
"""
def build_cross_signing_keys_data(user_data) -> dict:
return result
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
+47
View File
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
}
}
}
}
};
+7 -4
View File
@@ -147,10 +147,13 @@ export class MockRTCPeerConnection {
}
constructor() {
this.localDescription = {
const localDescriptionJSON = {
sdp: DUMMY_SDP,
type: "offer",
toJSON: function () {},
type: "offer" as RTCSdpType,
};
this.localDescription = {
toJSON: () => localDescriptionJSON,
...localDescriptionJSON,
};
this.readyToNegotiate = new Promise<void>((resolve) => {
@@ -265,7 +268,7 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
}
export class MockMediaStreamTrack {
+2 -2
View File
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
@@ -612,7 +612,7 @@ describe("Secrets", function () {
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
key_id: await encryptAESSecretStorageItem(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id,
"m.megolm_backup.v1",
+91 -37
View File
@@ -585,12 +585,15 @@ describe("MatrixRTCSession", () => {
it("creates a key when joining", () => {
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
expect(keys).toHaveLength(1);
const allKeys = sess!.getEncryptionKeys();
expect(allKeys).toBeTruthy();
expect(Array.from(allKeys)).toHaveLength(1);
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess?.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
expect.any(Uint8Array),
0,
"@alice:example.org:AAAAAAA",
);
});
it("sends keys when joining", async () => {
@@ -686,25 +689,27 @@ describe("MatrixRTCSession", () => {
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
it("Rotates key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
const firstKeysPayload = await keysSentPromise1;
expect(firstKeysPayload.keys).toHaveLength(1);
expect(firstKeysPayload.keys[0].index).toEqual(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
jest.advanceTimersByTime(10000);
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
const onMembershipsChanged = jest.fn();
@@ -719,9 +724,14 @@ describe("MatrixRTCSession", () => {
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
await keysSentPromise2;
jest.advanceTimersByTime(10000);
const secondKeysPayload = await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalled();
expect(secondKeysPayload.keys).toHaveLength(1);
expect(secondKeysPayload.keys[0].index).toEqual(1);
expect(secondKeysPayload.keys[0].key).not.toEqual(firstKeysPayload.keys[0].key);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
@@ -1197,9 +1207,16 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
});
@@ -1222,13 +1239,16 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[0]).toBeFalsy();
expect(bobKeys[1]).toBeFalsy();
expect(bobKeys[2]).toBeFalsy();
expect(bobKeys[3]).toBeFalsy();
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
4,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
});
@@ -1251,9 +1271,16 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
let bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
sess.onCallEncryption({
@@ -1272,9 +1299,20 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
encryptionKeyChangedListener.mockClear();
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
4,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(2);
});
@@ -1313,9 +1351,16 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(1000), // earlier timestamp than the newer key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("newer key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("newer key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(2);
});
@@ -1354,9 +1399,15 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(1000), // same timestamp as the first key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("second key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("second key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
});
it("ignores keys event for the local participant", () => {
@@ -1378,8 +1429,11 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const myKeys = sess.getKeysForParticipant(client.getUserId()!, client.getDeviceId()!)!;
expect(myKeys).toBeFalsy();
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(0);
});
+4 -55
View File
@@ -107,7 +107,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -120,7 +120,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(2);
});
@@ -133,7 +133,7 @@ describe("MSC3089TreeSpace", () => {
});
client.invite = fn;
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -155,61 +155,10 @@ describe("MSC3089TreeSpace", () => {
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
];
await tree.invite(target, true, false);
await tree.invite(target, true);
expect(fn).toHaveBeenCalledTimes(4);
});
it("should share keys with invitees", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
// Mock the history check as best as possible
const historyVis = "shared";
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
// We're not expecting a super rigid test: the function that calls this internally isn't
// really being tested here.
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
// Note: inverse test is implicit from other tests, which disable the call stack of this
// test in order to pass.
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(1);
expect(historyFn).toHaveBeenCalledTimes(1);
});
it("should not share keys with invitees if inappropriate history visibility", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
const historyVis = "joined"; // NOTE: Changed.
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(0);
expect(historyFn).toHaveBeenCalledTimes(1);
});
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
makePowerLevels(pls);
const fn = jest
+112 -9
View File
@@ -17,6 +17,7 @@
*/
import {
CollectStrategy,
Curve25519PublicKey,
Ed25519PublicKey,
HistoryVisibility as RustHistoryVisibility,
@@ -31,6 +32,7 @@ import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { defer } from "../../../src/utils";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { KnownMembership } from "../../../src/@types/membership";
import { DeviceIsolationMode, AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "../../../src/crypto-api";
describe("RoomEncryptor", () => {
describe("History Visibility", () => {
@@ -99,7 +101,7 @@ describe("RoomEncryptor", () => {
getEncryptionTargetMembers: jest.fn().mockReturnValue([mockRoomMember]),
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(true),
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Invited),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(null),
} as unknown as Mocked<Room>;
roomEncryptor = new RoomEncryptor(
@@ -111,6 +113,8 @@ describe("RoomEncryptor", () => {
);
});
const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
it("should ensure that there is only one shareRoomKey at a time", async () => {
const deferredShare = defer<void>();
const insideOlmShareRoom = defer<void>();
@@ -120,19 +124,19 @@ describe("RoomEncryptor", () => {
await deferredShare.promise;
});
roomEncryptor.prepareForEncryption(false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
await insideOlmShareRoom.promise;
// call several times more
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("World"), false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false, defaultDevicesIsolationMode);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("World"), false, defaultDevicesIsolationMode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(1);
deferredShare.resolve();
await roomEncryptor.prepareForEncryption(false);
await roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
// should have been called again
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(6);
@@ -158,8 +162,16 @@ describe("RoomEncryptor", () => {
let firstMessageFinished: string | null = null;
const firstRequest = roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
const secondRequest = roomEncryptor.encryptEvent(createMockEvent("Edit of Hello"), false);
const firstRequest = roomEncryptor.encryptEvent(
createMockEvent("Hello"),
false,
defaultDevicesIsolationMode,
);
const secondRequest = roomEncryptor.encryptEvent(
createMockEvent("Edit of Hello"),
false,
defaultDevicesIsolationMode,
);
firstRequest.then(() => {
if (firstMessageFinished === null) {
@@ -181,5 +193,96 @@ describe("RoomEncryptor", () => {
expect(firstMessageFinished).toBe("hello");
});
describe("DeviceIsolationMode", () => {
type TestCase = [
string,
{
mode: DeviceIsolationMode;
expectedStrategy: CollectStrategy;
globalBlacklistUnverifiedDevices: boolean;
},
];
const testCases: TestCase[] = [
[
"Share AllDevicesIsolationMode",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, false),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - with blacklist unverified",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, false),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned - blacklist true",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - Verified user problems true",
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, true),
globalBlacklistUnverifiedDevices: false,
},
],
[
'Share AllDevicesIsolationMode - with blacklist unverified - Verified user problems true"',
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, true),
globalBlacklistUnverifiedDevices: true,
},
],
];
let capturedSettings: CollectStrategy | undefined = undefined;
beforeEach(() => {
capturedSettings = undefined;
mockOlmMachine.shareRoomKey.mockImplementationOnce(async (roomId, users, encryptionSettings) => {
capturedSettings = encryptionSettings.sharingStrategy;
});
});
it.each(testCases)(
"prepareForEncryption should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.prepareForEncryption(globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
it.each(testCases)(
"encryptEvent should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.encryptEvent(createMockEvent("Hello"), globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
});
});
});
+43 -4
View File
@@ -69,7 +69,7 @@ import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
const TEST_USER = "@alice:example.com";
@@ -425,7 +425,7 @@ describe("initRustCrypto", () => {
}, 10000);
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
}
@@ -1362,13 +1362,52 @@ describe("RustCrypto", () => {
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
olmMachine.getIdentity.mockResolvedValue({
free: jest.fn(),
isVerified: jest.fn().mockReturnValue(true),
wasPreviouslyVerified: jest.fn().mockReturnValue(true),
});
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy();
});
});
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
+1 -1
View File
@@ -25,8 +25,8 @@ import {
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { calculateKeyCheck } from "../../src/crypto/aes";
import { randomString } from "../../src/randomstring";
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
@@ -0,0 +1,29 @@
/*
* Copyright 2024 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.
*/
/**
* An AES-encrypted secret storage payload.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface AESEncryptedSecretStoragePayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
+29 -36
View File
@@ -21,7 +21,7 @@ import { MsgType } from "../@types/event.ts";
*
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
* @see https://spec.matrix.org/v1.11/client-server-api/#extensions-to-mroommessage-msgtypes
*/
export interface EncryptedFile {
/**
@@ -84,13 +84,13 @@ interface BaseInfo {
*
* Used within `m.room.message` events that reference files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileInfo extends BaseInfo {
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
* Only present if the thumbnail is encrypted.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
thumbnail_file?: EncryptedFile;
/**
@@ -108,7 +108,7 @@ export interface FileInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference images.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
@@ -117,7 +117,7 @@ export interface ImageInfo extends FileInfo, ThumbnailInfo {}
*
* Used within `m.room.message` events that reference audio files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioInfo extends BaseInfo {
/**
@@ -131,7 +131,7 @@ export interface AudioInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference video files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoInfo extends AudioInfo, ImageInfo {
/**
@@ -148,30 +148,39 @@ export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
interface BaseContent {
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
file?: EncryptedFile;
/**
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
*/
url?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* A human-readable description of the file.
* This is recommended to be the filename of the original upload.
* If filename is not set or the value of both properties are identical,
* this is the filename of the original upload. Otherwise, this is a
* caption for the file.
*/
body: string;
/**
* The original filename of the uploaded file.
*/
filename?: string;
/**
* The format used in the `formatted_body`.
*/
format?: "org.matrix.custom.html";
/**
* The formatted version of the `body`, when it acts as a caption. This is required if `format` is specified.
*/
formatted_body?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* Information about the file referred to in url.
*/
@@ -185,15 +194,9 @@ export interface FileContent extends BaseContent {
/**
* Content format of media events with msgtype `m.image`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageContent extends BaseContent {
/**
* A textual representation of the image.
* This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. image attachment.
*/
body: string;
/**
* Metadata about the image referred to in url.
*/
@@ -207,14 +210,9 @@ export interface ImageContent extends BaseContent {
/**
* Content format of media events with msgtype `m.audio`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioContent extends BaseContent {
/**
* A description of the audio e.g. Bee Gees - Stayin Alive,
* or some kind of content description for accessibility e.g. audio attachment.
*/
body: string;
/**
* Metadata for the audio clip referred to in url.
*/
@@ -228,14 +226,9 @@ export interface AudioContent extends BaseContent {
/**
* Content format of media events with msgtype `m.video`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoContent extends BaseContent {
/**
* A description of the video e.g. Gangnam style,
* or some kind of content description for accessibility e.g. video attachment.
*/
body: string;
/**
* Metadata about the video clip referred to in url.
*/
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.
*/
// string of zeroes, for calculating the key check
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Calculate the MAC for checking the key.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
}
-37
View File
@@ -4087,43 +4087,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
await this.http.authedRequest(Method.Delete, path.path, path.queryData, undefined, { prefix: ClientPrefix.V3 });
}
/**
* Share shared-history decryption keys with the given users.
*
* @param roomId - the room for which keys should be shared.
* @param userIds - a list of users to share with. The keys will be sent to
* all of the user's current devices.
*
* @deprecated Do not use this method. It does not work with the Rust crypto stack, and even with the legacy
* stack it introduces a security vulnerability.
*/
public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
const roomEncryption = this.crypto?.getRoomEncryption(roomId);
if (!roomEncryption) {
// unknown room, or unencrypted room
this.logger.error("Unknown room. Not sharing decryption keys");
return;
}
const deviceInfos = await this.crypto.downloadKeys(userIds);
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
for (const [userId, devices] of deviceInfos) {
devicesByUser.set(userId, Array.from(devices.values()));
}
// XXX: Private member access
const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm);
if (alg.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
this.logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
}
/**
* Get the config for the media repository.
* @returns Promise which resolves with an object containing the config.
+86 -38
View File
@@ -41,11 +41,9 @@ export interface CryptoApi {
globalBlacklistUnverifiedDevices: boolean;
/**
* The cryptography mode to use.
*
* @see CryptoMode
* The {@link DeviceIsolationMode} mode to use.
*/
setCryptoMode(cryptoMode: CryptoMode): void;
setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void;
/**
* Return the current version of the crypto module.
@@ -197,6 +195,16 @@ export interface CryptoApi {
*/
getUserVerificationStatus(userId: string): Promise<UserVerificationStatus>;
/**
* "Pin" the current identity of the given user, accepting it as genuine.
*
* This is useful if the user has changed identity since we first saw them (leading to
* {@link UserVerificationStatus.needsUserApproval}), and we are now accepting their new identity.
*
* Throws an error if called on our own user ID, or on a user ID that we don't have an identity for.
*/
pinCurrentUserIdentity(userId: string): Promise<void>;
/**
* Get the verification status of a given device.
*
@@ -603,14 +611,13 @@ export enum DecryptionFailureCode {
/**
* The sender device is not cross-signed. This will only be used if the
* crypto mode is set to `CryptoMode.Invisible` or `CryptoMode.Transition`.
* device isolation mode is set to `OnlySignedDevicesIsolationMode`.
*/
UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE",
/**
* We weren't able to link the message back to any known device. This will
* only be used if the crypto mode is set to `CryptoMode.Invisible` or
* `CryptoMode.Transition`.
* only be used if the device isolation mode is set to `OnlySignedDevicesIsolationMode`.
*/
UNKNOWN_SENDER_DEVICE = "UNKNOWN_SENDER_DEVICE",
@@ -657,38 +664,59 @@ export enum DecryptionFailureCode {
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
}
/**
* The cryptography mode. Affects how messages are encrypted and decrypted.
* Only supported by Rust crypto.
*/
export enum CryptoMode {
/**
* Message encryption keys are shared with all devices in the room, except for
* blacklisted devices, or unverified devices if
* `globalBlacklistUnverifiedDevices` is set. Events from all senders are
* decrypted.
*/
Legacy,
/**
* Events are encrypted as with `Legacy` mode, but encryption will throw an error if a
* verified user has an unsigned device, or if a verified user replaces
* their identity. Events are decrypted only if they come from cross-signed
* devices, or devices that existed before the Rust crypto SDK started
* tracking device trust: other events will result in a decryption failure. (To access the failure
* reason, see {@link MatrixEvent.decryptionFailureReason}.)
*/
Transition,
/**
* Message encryption keys are only shared with devices that have been cross-signed by their owner.
* Encryption will throw an error if a verified user replaces their identity. Events are
* decrypted only if they come from a cross-signed device other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*/
Invisible,
/** Base {@link DeviceIsolationMode} kind. */
export enum DeviceIsolationModeKind {
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
}
/**
* A type of {@link DeviceIsolationMode}.
*
* Message encryption keys are shared with all devices in the room, except in case of
* verified user problems (see {@link errorOnVerifiedUserProblems}).
*
* Events from all senders are always decrypted (and should be decorated with message shields in case
* of authenticity warnings, see {@link EventEncryptionInfo}).
*/
export class AllDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
/**
*
* @param errorOnVerifiedUserProblems - Behavior when sharing keys to remote devices.
*
* If set to `true`, sharing keys will fail (i.e. message sending will fail) with an error if:
* - The user was previously verified but is not anymore, or:
* - A verified user has some unverified devices (not cross-signed).
*
* If `false`, the keys will be distributed as usual. In this case, the client UX should display
* warnings to inform the user about problematic devices/users, and stop them hitting this case.
*/
public constructor(public readonly errorOnVerifiedUserProblems: boolean) {}
}
/**
* A type of {@link DeviceIsolationMode}.
*
* Message encryption keys are only shared with devices that have been cross-signed by their owner.
* Encryption will throw an error if a verified user replaces their identity.
*
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*/
export class OnlySignedDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
}
/**
* DeviceIsolationMode represents the mode of device isolation used when encrypting or decrypting messages.
* It can be one of two types: {@link AllDevicesIsolationMode} or {@link OnlySignedDevicesIsolationMode}.
*
* Only supported by rust Crypto.
*/
export type DeviceIsolationMode = AllDevicesIsolationMode | OnlySignedDevicesIsolationMode;
/**
* Options object for `CryptoApi.bootstrapCrossSigning`.
*/
@@ -707,11 +735,29 @@ export interface BootstrapCrossSigningOpts {
* Represents the ways in which we trust a user
*/
export class UserVerificationStatus {
/**
* Indicates if the identity has changed in a way that needs user approval.
*
* This happens if the identity has changed since we first saw it, *unless* the new identity has also been verified
* by our user (eg via an interactive verification).
*
* To rectify this, either:
*
* * Conduct a verification of the new identity via {@link CryptoApi.requestVerificationDM}.
* * Pin the new identity, via {@link CryptoApi.pinCurrentUserIdentity}.
*
* @returns true if the identity has changed in a way that needs user approval.
*/
public readonly needsUserApproval: boolean;
public constructor(
private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean,
) {}
needsUserApproval: boolean = false,
) {
this.needsUserApproval = needsUserApproval;
}
/**
* @returns true if this user is verified via any means
@@ -737,6 +783,8 @@ export class UserVerificationStatus {
/**
* @returns true if this user's key is trusted on first use
*
* @deprecated No longer supported, with the Rust crypto stack.
*/
public isTofu(): boolean {
return this.tofu;
+2 -2
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { ISigned } from "../@types/signed.ts";
import { IEncryptedPayload } from "../crypto/aes.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
export interface Curve25519AuthData {
public_key: string;
@@ -77,7 +77,7 @@ export interface Curve25519SessionData {
}
/* eslint-disable camelcase */
export interface KeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecretStoragePayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
+4 -3
View File
@@ -22,7 +22,6 @@ import type { PkSigning } from "@matrix-org/olm";
import { IObject, pkSign, pkVerify } from "./olmlib.ts";
import { logger } from "../logger.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, encryptAES } from "./aes.ts";
import { DeviceInfo } from "./deviceinfo.ts";
import { ISignedKey, MatrixClient } from "../client.ts";
import { OlmDevice } from "./OlmDevice.ts";
@@ -36,6 +35,8 @@ import {
UserVerificationStatus as UserTrustLevel,
} from "../crypto-api/index.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
// backwards-compatibility re-exports
export { UserTrustLevel };
@@ -662,7 +663,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, type);
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, type);
return decodeBase64(decrypted);
} else {
return key;
@@ -676,7 +677,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, type);
return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
});
+9 -149
View File
@@ -14,153 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { decodeBase64, encodeBase64 } from "../base64.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
/**
* Encrypt a string using AES-CTR.
*
* @param data - the plaintext to encrypt
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
* encryption. Obviously, the same key must be provided when decrypting.
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
* so again the same value must be provided when decrypting.
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
*
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
* or generated), and an HMAC on the ciphertext all base64-encoded.
*/
export async function encryptAES(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<IEncryptedPayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
globalThis.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
/**
* Decrypt an AES-encrypted string.
*
* @param data - the encrypted data, returned by {@link encryptAES}.
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
* be the same as provided to {@link encryptAES}.
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
* key, so again must be the same as provided to {@link encryptAES}.
*/
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "HKDF",
salt: zeroSalt,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
info: new TextEncoder().encode(name),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
"encrypt",
"decrypt",
]);
const hmacProm = globalThis.crypto.subtle.importKey(
"raw",
hmacKey,
{
name: "HMAC",
hash: { name: "SHA-256" },
},
false,
["sign", "verify"],
);
return Promise.all([aesProm, hmacProm]);
}
// string of zeroes, for calculating the key check
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/** Calculate the MAC for checking the key.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
return encryptAES(ZERO_STR, key, "", iv);
}
// Export for backwards compatibility
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload };
// Export with new names instead of using `as` to not break react-sdk tests
export const encryptAES = encryptAESSecretStorageItem;
export const decryptAES = decryptAESSecretStorageItem;
export { calculateKeyCheck } from "../calculateKeyCheck.ts";
+11 -6
View File
@@ -27,7 +27,6 @@ import { DeviceTrustLevel } from "./CrossSigning.ts";
import { keyFromPassphrase } from "./key_passphrase.ts";
import { encodeUri, safeSet, sleep } from "../utils.ts";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
import {
Curve25519SessionData,
IAes256AuthData,
@@ -41,6 +40,10 @@ import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index.
import { BackupTrustInfo } from "../crypto-api/keybackup.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { encodeRecoveryKey } from "../crypto-api/index.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -94,7 +97,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | AESEncryptedSecretStoragePayload>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
@@ -825,22 +828,24 @@ export class Aes256 implements BackupAlgorithm {
return false;
}
public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
public encryptSession(data: Record<string, any>): Promise<AESEncryptedSecretStoragePayload> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
return encryptAESSecretStorageItem(JSON.stringify(plainText), this.key, data.session_id);
}
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
sessions: Record<string, IKeyBackupSession<AESEncryptedSecretStoragePayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
const decrypted = JSON.parse(
await decryptAESSecretStorageItem(sessionData.session_data, this.key, sessionId),
);
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
+4 -3
View File
@@ -19,11 +19,12 @@ import anotherjson from "another-json";
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, encryptAES } from "./aes.ts";
import { logger } from "../logger.ts";
import { Crypto } from "./index.ts";
import { Method } from "../http-api/index.ts";
import { SecretStorageKeyDescription } from "../secret-storage.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
@@ -61,7 +62,7 @@ export class DehydrationManager {
if (result) {
const { key, keyInfo, deviceDisplayName, time } = result;
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo;
this.deviceDisplayName = deviceDisplayName;
@@ -141,7 +142,7 @@ export class DehydrationManager {
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
const key = await encryptAESSecretStorageItem(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
keyInfo: this.keyInfo,
+23 -12
View File
@@ -47,7 +47,6 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts";
import { IllegalMethod } from "./verification/IllegalMethod.ts";
import { KeySignatureUploadError } from "../errors.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
import { DehydrationManager } from "./dehydration.ts";
import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup.ts";
import { IStore } from "../store/index.ts";
@@ -88,9 +87,9 @@ import {
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CrossSigningStatus,
CryptoMode,
decodeRecoveryKey,
DecryptionFailureCode,
DeviceIsolationMode,
DeviceVerificationStatus,
encodeRecoveryKey,
EventEncryptionInfo,
@@ -107,6 +106,10 @@ import { deviceInfoToDevice } from "./device-converter.ts";
import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import { KnownMembership } from "../@types/membership.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
/* re-exports for backwards compatibility */
export type {
@@ -650,12 +653,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
* Implementation of {@link Crypto.CryptoApi#setDeviceIsolationMode}.
*/
public setCryptoMode(cryptoMode: CryptoMode): void {
public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
throw new Error("Not supported");
}
/**
* Implementation of {@link Crypto.CryptoApi#getVersion}.
*/
@@ -1323,11 +1325,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns the key, if any, or null
*/
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
const encodedKey = await new Promise<Uint8Array | IEncryptedPayload | string | null>((resolve) => {
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
});
const encodedKey = await new Promise<Uint8Array | AESEncryptedSecretStoragePayload | string | null>(
(resolve) => {
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
},
);
let key: Uint8Array | null = null;
@@ -1338,7 +1342,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await decryptAES(encodedKey, pickleKey, "m.megolm_backup.v1");
const decrypted = await decryptAESSecretStorageItem(encodedKey, pickleKey, "m.megolm_backup.v1");
key = decodeBase64(decrypted);
}
return key;
@@ -1355,7 +1359,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
});
@@ -1609,6 +1613,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.checkUserTrust(userId);
}
/**
* Implementation of {@link Crypto.CryptoApi.pinCurrentUserIdentity}.
*/
public async pinCurrentUserIdentity(userId: string): Promise<void> {
throw new Error("not implemented");
}
/**
* Check whether a given device is trusted.
*
+3 -3
View File
@@ -25,8 +25,8 @@ import { Logger } from "../../logger.ts";
import { InboundGroupSessionData } from "../OlmDevice.ts";
import { MatrixEvent } from "../../models/event.ts";
import { DehydrationManager } from "../dehydration.ts";
import { IEncryptedPayload } from "../aes.ts";
import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
import { AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Internal module. Definitions for storage for the crypto module
@@ -35,11 +35,11 @@ import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
export interface SecretStorePrivateKeys {
"dehydration": {
keyInfo: DehydrationManager["keyInfo"];
key: IEncryptedPayload;
key: AESEncryptedSecretStoragePayload;
deviceDisplayName: string;
time: number;
} | null;
"m.megolm_backup.v1": IEncryptedPayload;
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
}
/**
+24 -4
View File
@@ -405,20 +405,40 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
/**
* Re-emit an EncryptionKeyChanged event for each tracked encryption key. This can be used to export
* the keys.
*/
public reemitEncryptionKeys(): void {
this.encryptionKeys.forEach((keys, participantId) => {
keys.forEach((key, index) => {
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId);
});
});
}
/**
* Get the known encryption keys for a given participant device.
*
* @param userId the user ID of the participant
* @param deviceId the device ID of the participant
* @returns The encryption keys for the given participant, or undefined if they are not known.
*
* @deprecated This will be made private in a future release.
*/
public getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
return this.getKeysForParticipantInternal(userId, deviceId);
}
private getKeysForParticipantInternal(userId: string, deviceId: string): Array<Uint8Array> | undefined {
return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key);
}
/**
* A map of keys used to encrypt and decrypt (we are using a symmetric
* cipher) given participant's media. This also includes our own key
*
* @deprecated This will be made private in a future release.
*/
public getEncryptionKeys(): IterableIterator<[string, Array<Uint8Array>]> {
// the returned array doesn't contain the timestamps
@@ -434,7 +454,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (!userId) throw new Error("No userId!");
if (!deviceId) throw new Error("No deviceId!");
return (this.getKeysForParticipant(userId, deviceId)?.length ?? 0) % 16;
return (this.getKeysForParticipantInternal(userId, deviceId)?.length ?? 0) % 16;
}
/**
@@ -776,8 +796,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestKeyEventSend();
logger.debug(`New member(s) have joined: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (oldFingerprints) {
// does it look like any of the members have updated their memberships?
const newFingerprints = this.lastMembershipFingerprints!;
@@ -788,7 +808,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) ||
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
if (candidateUpdates) {
logger.debug(`Member(s) have updated/reconnected: re-sending keys`);
logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`);
this.requestKeyEventSend();
}
}
+3 -18
View File
@@ -30,7 +30,6 @@ import {
simpleRetryOperation,
} from "../utils.ts";
import { MSC3089Branch } from "./MSC3089Branch.ts";
import { isRoomSharedHistory } from "../crypto/algorithms/megolm.ts";
import { ISendEventResponse } from "../@types/requests.ts";
import { FileType } from "../http-api/index.ts";
import { KnownMembership } from "../@types/membership.ts";
@@ -136,28 +135,14 @@ export class MSC3089TreeSpace {
* @param userId - The user ID to invite.
* @param andSubspaces - True (default) to invite the user to all
* directories/subspaces too, recursively.
* @param shareHistoryKeys - True (default) to share encryption keys
* with the invited user. This will allow them to decrypt the events (files)
* in the tree. Keys will not be shared if the room is lacking appropriate
* history visibility (by default, history visibility is "shared" in trees,
* which is an appropriate visibility for these purposes).
* @returns Promise which resolves when complete.
*/
public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> {
public async invite(userId: string, andSubspaces = true): Promise<void> {
const promises: Promise<void>[] = [this.retryInvite(userId)];
if (andSubspaces) {
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces, shareHistoryKeys)));
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces)));
}
return Promise.all(promises).then(() => {
// Note: key sharing is default on because for file trees it is relatively important that the invite
// target can actually decrypt the files. The implied use case is that by inviting a user to the tree
// it means the sender would like the receiver to view/download the files contained within, much like
// sharing a folder in other circles.
if (shareHistoryKeys && isRoomSharedHistory(this.room)) {
// noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
this.client.sendSharedHistoryKeys(this.roomId, [userId]);
}
});
await Promise.all(promises);
}
private retryInvite(userId: string): Promise<void> {
+1 -1
View File
@@ -2096,7 +2096,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
/**
* Calls {@link processPollEvent} for a list of events.
* Process a list of poll events.
*
* @param events - List of events
*/
+8 -5
View File
@@ -34,17 +34,20 @@ export type OidcRegistrationClientMetadata = {
policyUri: OidcRegistrationRequestBody["policy_uri"];
};
/**
* Request body for dynamic registration as defined by https://github.com/matrix-org/matrix-spec-proposals/pull/2966
*/
interface OidcRegistrationRequestBody {
client_name: string;
client_name?: string;
client_uri: string;
logo_uri?: string;
contacts: NonEmptyArray<string>;
tos_uri: string;
policy_uri: string;
contacts?: string[];
tos_uri?: string;
policy_uri?: string;
redirect_uris?: NonEmptyArray<string>;
response_types?: NonEmptyArray<string>;
grant_types?: NonEmptyArray<string>;
id_token_signed_response_alg: string;
id_token_signed_response_alg?: string;
token_endpoint_auth_method: string;
application_type: "web" | "native";
}
+45 -15
View File
@@ -36,6 +36,7 @@ import { HistoryVisibility } from "../@types/partials.ts";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts";
import { logDuration } from "../utils.ts";
import { KnownMembership } from "../@types/membership.ts";
import { DeviceIsolationMode, DeviceIsolationModeKind } from "../crypto-api/index.ts";
/**
* RoomEncryptor: responsible for encrypting messages to a given room
@@ -119,10 +120,15 @@ export class RoomEncryptor {
*
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
* in the room.
*
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
public async prepareForEncryption(globalBlacklistUnverifiedDevices: boolean): Promise<void> {
public async prepareForEncryption(
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
// We consider a prepareForEncryption as an encryption promise as it will potentially share keys
// even if it doesn't send an event.
// Usually this is called when the user starts typing, so we want to make sure we have keys ready when the
@@ -130,7 +136,7 @@ export class RoomEncryptor {
// If `encryptEvent` is invoked before `prepareForEncryption` has completed, the `encryptEvent` call will wait for
// `prepareForEncryption` to complete before executing.
// The part where `encryptEvent` shares the room key will then usually be a no-op as it was already performed by `prepareForEncryption`.
await this.encryptEvent(null, globalBlacklistUnverifiedDevices);
await this.encryptEvent(null, globalBlacklistUnverifiedDevices, deviceIsolationMode);
}
/**
@@ -140,9 +146,16 @@ export class RoomEncryptor {
* then, if an event is provided, encrypt it using the session.
*
* @param event - Event to be encrypted, or null if only preparing for encryption (in which case we will pre-share the room key).
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
public encryptEvent(event: MatrixEvent | null, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
public encryptEvent(
event: MatrixEvent | null,
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
const logger = new LogSpan(this.prefixedLogger, event ? (event.getTxnId() ?? "") : "prepareForEncryption");
// Ensure order of encryption to avoid message ordering issues, as the scheduler only ensures
// events order after they have been encrypted.
@@ -153,7 +166,7 @@ export class RoomEncryptor {
})
.then(async () => {
await logDuration(logger, "ensureEncryptionSession", async () => {
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices);
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices, deviceIsolationMode);
});
if (event) {
await logDuration(logger, "encryptEventInner", async () => {
@@ -173,9 +186,16 @@ export class RoomEncryptor {
* in the room.
*
* @param logger - a place to write diagnostics to
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
private async ensureEncryptionSession(logger: LogSpan, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
private async ensureEncryptionSession(
logger: LogSpan,
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
throw new Error(
`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`,
@@ -251,12 +271,22 @@ export class RoomEncryptor {
rustEncryptionSettings.rotationPeriodMessages = BigInt(this.encryptionSettings.rotation_period_msgs);
}
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) {
rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(true, false);
} else {
rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(false, false);
switch (deviceIsolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
{
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
const onlyAllowTrustedDevices =
this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(
onlyAllowTrustedDevices,
deviceIsolationMode.errorOnVerifiedUserProblems,
);
}
break;
case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
rustEncryptionSettings.sharingStrategy = CollectStrategy.identityBasedStrategy();
break;
}
await logDuration(this.prefixedLogger, "shareRoomKey", async () => {
+2 -2
View File
@@ -33,10 +33,10 @@ import { encodeUri, logDuration } from "../utils.ts";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
import { sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { IEncryptedPayload } from "../crypto/aes.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts";
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
import { IKeyBackup } from "../crypto/backup.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
@@ -622,7 +622,7 @@ export class RustBackupDecryptor implements BackupDecryptor {
* Implements {@link BackupDecryptor#decryptSessions}
*/
public async decryptSessions(
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | IEncryptedPayload>>,
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | AESEncryptedSecretStoragePayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
+3 -2
View File
@@ -19,7 +19,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Logger } from "../logger.ts";
import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/store/base.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, IEncryptedPayload } from "../crypto/aes.ts";
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
import { requestKeyBackupVersion } from "./backup.ts";
import { IRoomEncryption } from "../crypto/RoomList.ts";
@@ -28,6 +27,8 @@ import { RustCrypto } from "./rust-crypto.ts";
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
import { sleep } from "../utils.ts";
import { encodeBase64 } from "../base64.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Determine if any data needs migrating from the legacy store, and do so.
@@ -421,7 +422,7 @@ async function getAndDecryptCachedSecretKey(
});
if (key && key.ciphertext && key.iv && key.mac) {
return await decryptAES(key as IEncryptedPayload, legacyPickleKey, name);
return await decryptAESSecretStorageItem(key as AESEncryptedSecretStoragePayload, legacyPickleKey, name);
} else if (key instanceof Uint8Array) {
// This is a legacy backward compatibility case where the key was stored in clear.
return encodeBase64(key);
+44 -16
View File
@@ -45,7 +45,6 @@ import {
CrossSigningStatus,
CryptoApi,
CryptoCallbacks,
CryptoMode,
Curve25519AuthData,
DecryptionFailureCode,
DeviceVerificationStatus,
@@ -61,6 +60,9 @@ import {
VerificationRequest,
encodeRecoveryKey,
deriveRecoveryKeyFromPassphrase,
DeviceIsolationMode,
AllDevicesIsolationMode,
DeviceIsolationModeKind,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
@@ -107,7 +109,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000;
private _trustCrossSignedDevices = true;
private cryptoMode = CryptoMode.Legacy;
private deviceIsolationMode: DeviceIsolationMode = new AllDevicesIsolationMode(false);
/** whether {@link stop} has been called */
private stopped = false;
@@ -246,7 +248,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
}
await encryptor.encryptEvent(event, this.globalBlacklistUnverifiedDevices);
await encryptor.encryptEvent(event, this.globalBlacklistUnverifiedDevices, this.deviceIsolationMode);
}
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
@@ -259,7 +261,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// through decryptEvent and hence get rid of this case.
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
}
return await this.eventDecryptor.attemptEventDecryption(event, this.cryptoMode);
return await this.eventDecryptor.attemptEventDecryption(event, this.deviceIsolationMode);
}
/**
@@ -370,10 +372,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
/**
* Implementation of {@link Crypto.CryptoApi#setCryptoMode}.
* Implementation of {@link CryptoApi#setDeviceIsolationMode}.
*/
public setCryptoMode(cryptoMode: CryptoMode): void {
this.cryptoMode = cryptoMode;
public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
this.deviceIsolationMode = isolationMode;
}
/**
@@ -398,7 +400,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const encryptor = this.roomEncryptors[room.roomId];
if (encryptor) {
encryptor.prepareForEncryption(this.globalBlacklistUnverifiedDevices);
encryptor.prepareForEncryption(this.globalBlacklistUnverifiedDevices, this.deviceIsolationMode);
}
}
@@ -654,9 +656,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
if (userIdentity === undefined) {
return new UserVerificationStatus(false, false, false);
}
const verified = userIdentity.isVerified();
const wasVerified = userIdentity.wasPreviouslyVerified();
const needsUserApproval =
userIdentity instanceof RustSdkCryptoJs.UserIdentity ? userIdentity.identityNeedsUserApproval() : false;
userIdentity.free();
return new UserVerificationStatus(verified, false, false);
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
}
/**
* Implementation of {@link CryptoApi#pinCurrentUserIdentity}.
*/
public async pinCurrentUserIdentity(userId: string): Promise<void> {
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
if (userIdentity === undefined) {
throw new Error("Cannot pin identity of unknown user");
}
if (userIdentity instanceof RustSdkCryptoJs.OwnUserIdentity) {
throw new Error("Cannot pin identity of own user");
}
await userIdentity.pinCurrentMasterKey();
}
/**
@@ -777,6 +801,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// Create a new storage key and add it to secret storage
this.logger.info("bootstrapSecretStorage: creating new secret storage key");
const recoveryKey = await createSecretStorageKey();
if (!recoveryKey) {
throw new Error("createSecretStorageKey() callback did not return a secret storage key");
}
await this.addSecretStorageKeyToSecretStorage(recoveryKey);
}
@@ -1751,7 +1778,10 @@ class EventDecryptor {
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader,
) {}
public async attemptEventDecryption(event: MatrixEvent, cryptoMode: CryptoMode): Promise<IEventDecryptionResult> {
public async attemptEventDecryption(
event: MatrixEvent,
isolationMode: DeviceIsolationMode,
): Promise<IEventDecryptionResult> {
// add the event to the pending list *before* attempting to decrypt.
// then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry.
@@ -1759,16 +1789,14 @@ class EventDecryptor {
this.addEventToPendingList(event);
let trustRequirement;
switch (cryptoMode) {
case CryptoMode.Legacy:
switch (isolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted;
break;
case CryptoMode.Transition:
case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy;
break;
case CryptoMode.Invisible:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSigned;
break;
}
try {
+12 -9
View File
@@ -23,9 +23,12 @@ limitations under the License.
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
import { MatrixEvent } from "./models/event.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./crypto/aes.ts";
import { randomString } from "./randomstring.ts";
import { logger } from "./logger.ts";
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "./crypto/aes.ts";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -200,13 +203,13 @@ export interface SecretStorageCallbacks {
interface SecretInfo {
encrypted: {
[keyId: string]: IEncryptedPayload;
[keyId: string]: AESEncryptedSecretStoragePayload;
};
}
interface Decryptors {
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
encrypt: (plaintext: string) => Promise<AESEncryptedSecretStoragePayload>;
decrypt: (ciphertext: AESEncryptedSecretStoragePayload) => Promise<string>;
}
/**
@@ -491,7 +494,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
const encrypted: Record<string, IEncryptedPayload> = {};
const encrypted: Record<string, AESEncryptedSecretStoragePayload> = {};
if (!keys) {
const defaultKeyId = await this.getDefaultKeyId();
@@ -638,11 +641,11 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: function (secret: string): Promise<IEncryptedPayload> {
return encryptAES(secret, privateKey, name);
encrypt: function (secret: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(secret, privateKey, name);
},
decrypt: function (encInfo: IEncryptedPayload): Promise<string> {
return decryptAES(encInfo, privateKey, name);
decrypt: function (encInfo: AESEncryptedSecretStoragePayload): Promise<string> {
return decryptAESSecretStorageItem(encInfo, privateKey, name);
},
};
return [keyId, decryption];
+1
View File
@@ -26,6 +26,7 @@ export * from "./@types/membership.ts";
export type * from "./@types/event.ts";
export type * from "./@types/events.ts";
export type * from "./@types/state_events.ts";
export type * from "./@types/AESEncryptedSecretStoragePayload.ts";
/** The different methods for device and user verification */
export enum VerificationMethod {
+54
View File
@@ -0,0 +1,54 @@
/*
* Copyright 2024 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 { decodeBase64 } from "../base64.ts";
import { deriveKeys } from "./internal/deriveKeys.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Decrypt an AES-encrypted Secret Storage item.
*
* @param data - the encrypted data, returned by {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
* be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
* key, so again must be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
*/
export default async function decryptAESSecretStorageItem(
data: AESEncryptedSecretStoragePayload,
key: Uint8Array,
name: string,
): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2024 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 { decodeBase64, encodeBase64 } from "../base64.ts";
import { deriveKeys } from "./internal/deriveKeys.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Encrypt a string as a secret storage item, using AES-CTR.
*
* @param data - the plaintext to encrypt
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
* encryption. Obviously, the same key must be provided when decrypting.
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
* so again the same value must be provided when decrypting.
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
*
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
* or generated), and an HMAC on the ciphertext all base64-encoded.
*/
export default async function encryptAESSecretStorageItem(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<AESEncryptedSecretStoragePayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
globalThis.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2024 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.
*/
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
/**
* Derive AES and HMAC keys from a master key.
*
* This is used for deriving secret storage keys: see https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2 (step 1).
*
* @param key
* @param name
*/
export async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "HKDF",
salt: zeroSalt,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
info: new TextEncoder().encode(name),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
"encrypt",
"decrypt",
]);
const hmacProm = globalThis.crypto.subtle.importKey(
"raw",
hmacKey,
{
name: "HMAC",
hash: { name: "SHA-256" },
},
false,
["sign", "verify"],
);
return Promise.all([aesProm, hmacProm]);
}
+3 -3
View File
@@ -2024,7 +2024,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.sendVoipEvent(EventType.CallNegotiate, {
lifetime: CALL_TIMEOUT_MS,
description: this.peerConn!.localDescription?.toJSON(),
description: this.peerConn!.localDescription?.toJSON() as RTCSessionDescription,
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
});
}
@@ -2152,9 +2152,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) {
content.offer = this.peerConn!.localDescription?.toJSON();
content.offer = this.peerConn!.localDescription?.toJSON() as RTCSessionDescription;
} else {
content.description = this.peerConn!.localDescription?.toJSON();
content.description = this.peerConn!.localDescription?.toJSON() as RTCSessionDescription;
}
content.capabilities = {
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
"coverageLabel": "TypeDoc",
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts"],
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts", "src/utils/*.ts"],
"excludeExternals": true,
"out": "_docs"
}
+403 -144
View File
@@ -1454,9 +1454,9 @@
"@jridgewell/sourcemap-codec" "^1.4.14"
"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329"
integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw==
version "9.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5"
integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==
"@matrix-org/olm@3.2.15":
version "3.2.15"
@@ -1553,12 +1553,52 @@
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@shikijs/core@1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.14.1.tgz#008f1c4a20ff83fd1672d9e31d76b687862f7511"
integrity sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@shikijs/core@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.18.0.tgz#30dde8e53026dada606c4cf7f32d80a3f33d437c"
integrity sha512-VK4BNVCd2leY62Nm2JjyxtRLkyrZT/tv104O81eyaCjHq4Adceq2uJVFJJAIof6lT1mBwZrEo2qT/T+grv3MQQ==
dependencies:
"@shikijs/engine-javascript" "1.18.0"
"@shikijs/engine-oniguruma" "1.18.0"
"@shikijs/types" "1.18.0"
"@shikijs/vscode-textmate" "^9.2.2"
"@types/hast" "^3.0.4"
hast-util-to-html "^9.0.3"
"@shikijs/engine-javascript@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.18.0.tgz#9888011c5d869a687b42e3e56c7243f15a73524b"
integrity sha512-qoP/aO/ATNwYAUw1YMdaip/YVEstMZEgrwhePm83Ll9OeQPuxDZd48szZR8oSQNQBT8m8UlWxZv8EA3lFuyI5A==
dependencies:
"@shikijs/types" "1.18.0"
"@shikijs/vscode-textmate" "^9.2.2"
oniguruma-to-js "0.4.3"
"@shikijs/engine-oniguruma@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.18.0.tgz#7e57fd19b62b18cf2de382da684d042ee934f65d"
integrity sha512-B9u0ZKI/cud+TcmF8Chyh+R4V5qQVvyDOqXC2l2a4x73PBSBc6sZ0JRAX3eqyJswqir6ktwApUUGBYePdKnMJg==
dependencies:
"@shikijs/types" "1.18.0"
"@shikijs/vscode-textmate" "^9.2.2"
"@shikijs/types@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.18.0.tgz#4c2d62d17f78cbfc051a15480ab4dfb0f06196c9"
integrity sha512-O9N36UEaGGrxv1yUrN2nye7gDLG5Uq0/c1LyfmxsvzNPqlHzWo9DI0A4+fhW2y3bGKuQu/fwS7EPdKJJCowcVA==
dependencies:
"@shikijs/vscode-textmate" "^9.2.2"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz#24571f50625c7cd075f9efe0def8b9d2c0930ada"
integrity sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==
"@sinclair/typebox@^0.24.1":
version "0.24.51"
@@ -1688,7 +1728,7 @@
dependencies:
"@types/node" "*"
"@types/hast@^3.0.4":
"@types/hast@^3.0.0", "@types/hast@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
@@ -1736,6 +1776,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
dependencies:
"@types/unist" "*"
"@types/ms@*":
version "0.7.34"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
@@ -1749,9 +1796,9 @@
undici-types "~5.26.4"
"@types/node@18":
version "18.19.48"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.48.tgz#3a1696f4a7298d8831ed9ce47db62bf4c62c8880"
integrity sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==
version "18.19.50"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.50.tgz#8652b34ee7c0e7e2004b3f08192281808d41bf5a"
integrity sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==
dependencies:
undici-types "~5.26.4"
@@ -1780,7 +1827,7 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
"@types/unist@*":
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
@@ -1836,13 +1883,13 @@
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/scope-manager@8.4.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz#8a13d3c0044513d7960348db6f4789d2a06fa4b4"
integrity sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==
"@typescript-eslint/scope-manager@8.6.0":
version "8.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz#28cc2fc26a84b75addf45091a2c6283e29e2c982"
integrity sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==
dependencies:
"@typescript-eslint/types" "8.4.0"
"@typescript-eslint/visitor-keys" "8.4.0"
"@typescript-eslint/types" "8.6.0"
"@typescript-eslint/visitor-keys" "8.6.0"
"@typescript-eslint/type-utils@7.18.0":
version "7.18.0"
@@ -1859,10 +1906,10 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9"
integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==
"@typescript-eslint/types@8.4.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.4.0.tgz#b44d6a90a317a6d97a3e5fabda5196089eec6171"
integrity sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==
"@typescript-eslint/types@8.6.0":
version "8.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.6.0.tgz#cdc3a16f83f2f0663d6723e9fd032331cdd9f51c"
integrity sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==
"@typescript-eslint/typescript-estree@7.18.0":
version "7.18.0"
@@ -1878,13 +1925,13 @@
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/typescript-estree@8.4.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz#00ed79ae049e124db37315cde1531a900a048482"
integrity sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==
"@typescript-eslint/typescript-estree@8.6.0":
version "8.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz#f945506de42871f04868371cb5bf21e8f7266e01"
integrity sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==
dependencies:
"@typescript-eslint/types" "8.4.0"
"@typescript-eslint/visitor-keys" "8.4.0"
"@typescript-eslint/types" "8.6.0"
"@typescript-eslint/visitor-keys" "8.6.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -1903,14 +1950,14 @@
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.4.0.tgz#35c552a404858c853a1f62ba6df2214f1988afc3"
integrity sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==
version "8.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.6.0.tgz#175fe893f32804bed1e72b3364ea6bbe1044181c"
integrity sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "8.4.0"
"@typescript-eslint/types" "8.4.0"
"@typescript-eslint/typescript-estree" "8.4.0"
"@typescript-eslint/scope-manager" "8.6.0"
"@typescript-eslint/types" "8.6.0"
"@typescript-eslint/typescript-estree" "8.6.0"
"@typescript-eslint/visitor-keys@7.18.0":
version "7.18.0"
@@ -1920,15 +1967,15 @@
"@typescript-eslint/types" "7.18.0"
eslint-visitor-keys "^3.4.3"
"@typescript-eslint/visitor-keys@8.4.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz#1e8a8b8fd3647db1e42361fdd8de3e1679dec9d2"
integrity sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==
"@typescript-eslint/visitor-keys@8.6.0":
version "8.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz#5432af4a1753f376f35ab5b891fc9db237aaf76f"
integrity sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==
dependencies:
"@typescript-eslint/types" "8.4.0"
"@typescript-eslint/types" "8.6.0"
eslint-visitor-keys "^3.4.3"
"@ungap/structured-clone@^1.2.0":
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
@@ -2023,9 +2070,9 @@ ansi-regex@^5.0.1:
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
version "6.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
ansi-styles@^3.2.1:
version "3.2.1"
@@ -2089,7 +2136,7 @@ array-buffer-byte-length@^1.0.1:
call-bind "^1.0.5"
is-array-buffer "^3.0.4"
array-includes@^3.1.7:
array-includes@^3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
@@ -2106,7 +2153,7 @@ array-union@^2.1.0:
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array.prototype.findlastindex@^1.2.3:
array.prototype.findlastindex@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d"
integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==
@@ -2366,6 +2413,11 @@ caniuse-lite@^1.0.30001646:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f"
integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
chalk@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"
@@ -2398,6 +2450,16 @@ char-regex@^1.0.2:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
character-entities-legacy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@@ -2515,6 +2577,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
commander@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@@ -2650,11 +2717,11 @@ data-view-byte-offset@^1.0.0:
is-data-view "^1.0.1"
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@~4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
dependencies:
ms "2.1.2"
ms "^2.1.3"
debug@^3.2.7:
version "3.2.7"
@@ -2713,7 +2780,7 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dequal@^2.0.3:
dequal@^2.0.0, dequal@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
@@ -2723,6 +2790,13 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
devlop@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
dependencies:
dequal "^2.0.0"
diff-sequences@^28.1.1:
version "28.1.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6"
@@ -2791,9 +2865,9 @@ emittery@^0.13.1:
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
emoji-regex@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23"
integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==
version "10.4.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -3000,13 +3074,6 @@ eslint-import-resolver-typescript@^3.5.1:
is-bun-module "^1.0.2"
is-glob "^4.0.3"
eslint-module-utils@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34"
integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==
dependencies:
debug "^3.2.7"
eslint-module-utils@^2.8.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342"
@@ -3014,6 +3081,13 @@ eslint-module-utils@^2.8.1:
dependencies:
debug "^3.2.7"
eslint-module-utils@^2.9.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz#b99b211ca4318243f09661fae088f373ad5243c4"
integrity sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==
dependencies:
debug "^3.2.7"
eslint-plugin-es@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
@@ -3023,39 +3097,40 @@ eslint-plugin-es@^3.0.0:
regexpp "^3.0.0"
eslint-plugin-import@^2.26.0:
version "2.29.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
version "2.30.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449"
integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==
dependencies:
array-includes "^3.1.7"
array.prototype.findlastindex "^1.2.3"
"@rtsao/scc" "^1.1.0"
array-includes "^3.1.8"
array.prototype.findlastindex "^1.2.5"
array.prototype.flat "^1.3.2"
array.prototype.flatmap "^1.3.2"
debug "^3.2.7"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.8.0"
hasown "^2.0.0"
is-core-module "^2.13.1"
eslint-module-utils "^2.9.0"
hasown "^2.0.2"
is-core-module "^2.15.1"
is-glob "^4.0.3"
minimatch "^3.1.2"
object.fromentries "^2.0.7"
object.groupby "^1.0.1"
object.values "^1.1.7"
object.fromentries "^2.0.8"
object.groupby "^1.0.3"
object.values "^1.2.0"
semver "^6.3.1"
tsconfig-paths "^3.15.0"
eslint-plugin-jest@^28.0.0:
version "28.8.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.8.2.tgz#7f8307179c5cf8d51101b3aa002be168daadecbc"
integrity sha512-mC3OyklHmS5i7wYU1rGId9EnxRI8TVlnFG56AE+8U9iRy6zwaNygZR+DsdZuCL0gRG0wVeyzq+uWcPt6yJrrMA==
version "28.8.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz#c5699bba0ad06090ad613535e4f1572f4c2567c0"
integrity sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==
dependencies:
"@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
eslint-plugin-jsdoc@^50.0.0:
version "50.2.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.2.tgz#151e63c8bc245ea8b2357d4229392a5b993827b0"
integrity sha512-i0ZMWA199DG7sjxlzXn5AeYZxpRfMJjDPUl7lL9eJJX8TPRoIaxJU4ys/joP5faM5AXE1eqW/dslCj3uj4Nqpg==
version "50.2.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.4.tgz#9abef68ea5869e87d8a4444bfef9e5a7787162e2"
integrity sha512-020jA+dXaXdb+TML3ZJBvpPmzwbNROjnYuTYi/g6A5QEmEjhptz4oPJDKkOGMIByNxsPpdTLzSU1HYVqebOX1w==
dependencies:
"@es-joy/jsdoccomment" "~0.48.0"
are-docs-informative "^0.0.2"
@@ -3709,6 +3784,30 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hast-util-to-html@^9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b"
integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
ccount "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^3.0.0"
html-void-elements "^3.0.0"
mdast-util-to-hast "^13.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
stringify-entities "^4.0.0"
zwitch "^2.0.4"
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
dependencies:
"@types/hast" "^3.0.0"
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -3726,6 +3825,11 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
@@ -3754,9 +3858,9 @@ human-signals@^5.0.0:
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
husky@^9.0.0:
version "9.1.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.5.tgz#2b6edede53ee1adbbd3a3da490628a23f5243b83"
integrity sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==
version "9.1.6"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.6.tgz#e23aa996b6203ab33534bdc82306b0cf2cb07d6c"
integrity sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==
iconv-lite@0.6.3:
version "0.6.3"
@@ -3872,20 +3976,13 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-core-module@^2.13.0:
is-core-module@^2.13.0, is-core-module@^2.15.1:
version "2.15.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
dependencies:
hasown "^2.0.2"
is-core-module@^2.13.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1"
integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==
dependencies:
hasown "^2.0.2"
is-data-view@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f"
@@ -4657,9 +4754,9 @@ kleur@^3.0.3:
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
knip@^5.0.0:
version "5.29.1"
resolved "https://registry.yarnpkg.com/knip/-/knip-5.29.1.tgz#596ba9aea44fb56d89c9cc7d73e2522d50280713"
integrity sha512-l8qFtRqNpCk8xf46VOwhBUva7LBwanoGPJ4KQNwVRl6hmEXStf1BJlfbYRZ+yQpbilbIV6LN+ztX6LaGtyd4TQ==
version "5.30.2"
resolved "https://registry.yarnpkg.com/knip/-/knip-5.30.2.tgz#3cee8df2e1eb0c858f81e64568b72c0a23b94479"
integrity sha512-UuUwuTN+6Aa6mjxufWwidayGX/tPJsxZSlwUo8q4R+Gf/0aNYuhHsUH/GccuKtRPfFnf3r+ZU/7BV0dNfC7OEQ==
dependencies:
"@nodelib/fs.walk" "1.2.8"
"@snyk/github-codeowners" "1.1.0"
@@ -4709,9 +4806,9 @@ linkify-it@^5.0.0:
uc.micro "^2.0.0"
lint-staged@^15.0.2:
version "15.2.9"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.9.tgz#bf70d40b6b192df6ad756fb89822211615e0f4da"
integrity sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==
version "15.2.10"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2"
integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==
dependencies:
chalk "~5.3.0"
commander "~12.1.0"
@@ -4719,7 +4816,7 @@ lint-staged@^15.0.2:
execa "~8.0.1"
lilconfig "~3.1.2"
listr2 "~8.2.4"
micromatch "~4.0.7"
micromatch "~4.0.8"
pidtree "~0.6.0"
string-argv "~0.3.2"
yaml "~2.5.0"
@@ -4782,9 +4879,9 @@ log-update@^6.1.0:
wrap-ansi "^9.0.0"
loglevel@^1.7.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7"
integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==
version "1.9.2"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==
lru-cache@^11.0.0:
version "11.0.0"
@@ -4862,6 +4959,21 @@ matrix-widget-api@^1.8.2:
"@types/events" "^3.0.0"
events "^3.2.0"
mdast-util-to-hast@^13.0.0:
version "13.2.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
"@ungap/structured-clone" "^1.0.0"
devlop "^1.0.0"
micromark-util-sanitize-uri "^2.0.0"
trim-lines "^3.0.0"
unist-util-position "^5.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
@@ -4877,7 +4989,39 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
micromark-util-character@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1"
integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
micromark-util-sanitize-uri@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-symbol@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
micromark-util-types@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
micromatch@^4.0.4, micromatch@~4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -4885,14 +5029,6 @@ micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
micromatch@~4.0.7:
version "4.0.7"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
@@ -4961,12 +5097,7 @@ mkdirp@1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1:
ms@^2.1.1, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -5047,7 +5178,7 @@ object.assign@^4.1.5:
has-symbols "^1.0.3"
object-keys "^1.1.1"
object.fromentries@^2.0.7:
object.fromentries@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
@@ -5057,7 +5188,7 @@ object.fromentries@^2.0.7:
es-abstract "^1.23.2"
es-object-atoms "^1.0.0"
object.groupby@^1.0.1:
object.groupby@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
@@ -5066,7 +5197,7 @@ object.groupby@^1.0.1:
define-properties "^1.2.1"
es-abstract "^1.23.2"
object.values@^1.1.7:
object.values@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
@@ -5110,6 +5241,13 @@ onetime@^7.0.0:
dependencies:
mimic-function "^5.0.0"
oniguruma-to-js@0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740"
integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==
dependencies:
regex "^4.3.2"
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
@@ -5346,6 +5484,11 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
property-information@^6.0.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
psl@^1.1.33:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
@@ -5448,6 +5591,11 @@ regenerator-transform@^0.15.2:
dependencies:
"@babel/runtime" "^7.8.4"
regex@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/regex/-/regex-4.3.2.tgz#a68a68c9b337a77bf4ce4ed0b4b1a49d97cb3b7b"
integrity sha512-kK/AA3A9K6q2js89+VMymcboLOlF5lZRCYJv3gzszXFHBr6kO6qLGzbm+UIugBEV8SMMKCTR59txoY6ctRHYVw==
regexp-tree@^0.1.27:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
@@ -5690,12 +5838,16 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shiki@^1.9.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.14.1.tgz#617e62dfbe3a083e46111e22086044fbd7644786"
integrity sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==
shiki@^1.16.2:
version "1.18.0"
resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.18.0.tgz#4f9ca2f442b3612849017ab1dcac47c35ee52276"
integrity sha512-8jo7tOXr96h9PBQmOHVrltnETn1honZZY76YA79MHheGQg55jBvbm9dtU+MI5pjC5NJCFuA6rvVTLVeSW5cE4A==
dependencies:
"@shikijs/core" "1.14.1"
"@shikijs/core" "1.18.0"
"@shikijs/engine-javascript" "1.18.0"
"@shikijs/engine-oniguruma" "1.18.0"
"@shikijs/types" "1.18.0"
"@shikijs/vscode-textmate" "^9.2.2"
"@types/hast" "^3.0.4"
side-channel@^1.0.4:
@@ -5772,6 +5924,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@@ -5831,7 +5988,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5886,7 +6052,22 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
stringify-entities@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3"
integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
dependencies:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6046,6 +6227,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
ts-api-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
@@ -6080,11 +6266,16 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2:
tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.1:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tslib@^2.6.2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -6167,9 +6358,9 @@ typedoc-plugin-coverage@^3.0.0:
integrity sha512-wpywQ95tqGSD6IbYUPMXSKiwnSWboSKdx2y9X6SJQKzQvBqZoz5iiUyDJFixtW8v7+xmrqXFR/B6Wy37FNhVqA==
typedoc-plugin-mdn-links@^3.0.3:
version "3.2.11"
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.2.11.tgz#837ab7138535a91d9e1803bf69b6e357b6d0c354"
integrity sha512-0VlF21O3S1L373UTkUFleoQDrgkh5quAqjCVusBaa3czLahK6LsUxQj6PRqbT5xN0emUVYnT7UTwe8haU2MFrw==
version "3.3.1"
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.3.1.tgz#cf9962341477437c2235a640e006d1da48551024"
integrity sha512-hgCOm3vDYLbSsIiQbr5hb/ayiMnspjsOwgH4hNVoOLaAeEecuwlY4VY7yh68TY10P1GwbG3N4UnAqmbkvf89KA==
typedoc-plugin-missing-exports@^3.0.0:
version "3.0.0"
@@ -6177,20 +6368,20 @@ typedoc-plugin-missing-exports@^3.0.0:
integrity sha512-R7D8fYrK34mBFZSlF1EqJxfqiUSlQSmyrCiQgTQD52nNm6+kUtqwiaqaNkuJ2rA2wBgWFecUA8JzHT7x2r7ePg==
typedoc@^0.26.0:
version "0.26.6"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.6.tgz#9cb3d6f0ed5070f86af169c3f88ca2c9b7031f59"
integrity sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==
version "0.26.7"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.7.tgz#1980e3ed51c6c315b7a09786b2b9af1106a3aa80"
integrity sha512-gUeI/Wk99vjXXMi8kanwzyhmeFEGv1LTdTQsiyIsmSYsBebvFxhbcyAx7Zjo4cMbpLGxM4Uz3jVIjksu/I2v6Q==
dependencies:
lunr "^2.3.9"
markdown-it "^14.1.0"
minimatch "^9.0.5"
shiki "^1.9.1"
yaml "^2.4.5"
shiki "^1.16.2"
yaml "^2.5.1"
typescript@^5.3.3:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.4.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
@@ -6240,6 +6431,44 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-position@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
dependencies:
"@types/unist" "^3.0.0"
unist-util-visit-parents@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -6300,6 +6529,22 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
vfile-message@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vfile@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
dependencies:
"@types/unist" "^3.0.0"
vfile-message "^4.0.0"
w3c-xmlserializer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"
@@ -6418,7 +6663,16 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -6488,10 +6742,10 @@ yallist@^3.0.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yaml@^2.4.5, yaml@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d"
integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==
yaml@^2.5.1, yaml@~2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"
integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==
yargs-parser@^21.1.1:
version "21.1.1"
@@ -6522,11 +6776,16 @@ yocto-queue@^0.1.0:
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod-validation-error@^3.0.3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.1.tgz#86adc781129d1a7fed3c3e567e8dbe7c4a15eaa4"
integrity sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==
version "3.4.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6"
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
zod@^3.22.4:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==