summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
committerRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
commit953ddd82e48dd206cef5ac94456549aed13b3ad5 (patch)
tree8f003106ee2e7f422e5a22d2ee04d0db302e66c0 /includes/external/matrix/node_modules/matrix-js-sdk/src/crypto
parent62a9199846b0c07c03218703b33e8385764f42d9 (diff)
downloadpluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.gz
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.bz2
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.zip
Updated 30 files and deleted 2976 files (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/crypto')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts803
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts989
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts356
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts1496
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts485
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts63
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts583
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts157
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts268
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts20
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts2208
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts329
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts127
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts813
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts271
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts161
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts3936
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts93
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts77
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts566
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts62
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts226
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts1062
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts708
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts403
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts533
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts369
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts76
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts311
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts492
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts37
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts34
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts356
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts354
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts926
37 files changed, 0 insertions, 19850 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts
deleted file mode 100644
index 31ed2d4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts
+++ /dev/null
@@ -1,803 +0,0 @@
-/*
-Copyright 2019 - 2021 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.
-*/
-
-/**
- * Cross signing methods
- */
-
-import { PkSigning } from "@matrix-org/olm";
-
-import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from "./olmlib";
-import { logger } from "../logger";
-import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
-import { decryptAES, encryptAES } from "./aes";
-import { DeviceInfo } from "./deviceinfo";
-import { SecretStorage } from "./SecretStorage";
-import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
-import { OlmDevice } from "./OlmDevice";
-import { ICryptoCallbacks } from ".";
-import { ISignatures } from "../@types/signed";
-import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
-
-function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
- // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
- // We assume only a single key, and we want the bare form without type
- // prefix, so we select the values.
- return Object.values(keyInfo.keys)[0];
-}
-
-export interface ICacheCallbacks {
- getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array | null>;
- storeCrossSigningKeyCache?(type: string, key?: Uint8Array): Promise<void>;
-}
-
-export interface ICrossSigningInfo {
- keys: Record<string, ICrossSigningKey>;
- firstUse: boolean;
- crossSigningVerifiedBefore: boolean;
-}
-
-export class CrossSigningInfo {
- public keys: Record<string, ICrossSigningKey> = {};
- public firstUse = true;
- // This tracks whether we've ever verified this user with any identity.
- // When you verify a user, any devices online at the time that receive
- // the verifying signature via the homeserver will latch this to true
- // and can use it in the future to detect cases where the user has
- // become unverified later for any reason.
- private crossSigningVerifiedBefore = false;
-
- /**
- * Information about a user's cross-signing keys
- *
- * @param userId - the user that the information is about
- * @param callbacks - Callbacks used to interact with the app
- * Requires getCrossSigningKey and saveCrossSigningKeys
- * @param cacheCallbacks - Callbacks used to interact with the cache
- */
- public constructor(
- public readonly userId: string,
- private callbacks: ICryptoCallbacks = {},
- private cacheCallbacks: ICacheCallbacks = {},
- ) {}
-
- public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
- const res = new CrossSigningInfo(userId);
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- // @ts-ignore - ts doesn't like this and nor should we
- res[prop] = obj[prop];
- }
- }
- return res;
- }
-
- public toStorage(): ICrossSigningInfo {
- return {
- keys: this.keys,
- firstUse: this.firstUse,
- crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
- };
- }
-
- /**
- * Calls the app callback to ask for a private key
- *
- * @param type - The key type ("master", "self_signing", or "user_signing")
- * @param expectedPubkey - The matching public key or undefined to use
- * the stored public key for the given key type.
- * @returns An array with [ public key, Olm.PkSigning ]
- */
- public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
- const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
-
- if (!this.callbacks.getCrossSigningKey) {
- throw new Error("No getCrossSigningKey callback supplied");
- }
-
- if (expectedPubkey === undefined) {
- expectedPubkey = this.getId(type)!;
- }
-
- function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
- if (!key) return;
- const signing = new global.Olm.PkSigning();
- const gotPubkey = signing.init_with_seed(key);
- if (gotPubkey === expectedPubkey) {
- return [gotPubkey, signing];
- }
- signing.free();
- }
-
- let privkey: Uint8Array | null = null;
- if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
- privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
- }
-
- const cacheresult = validateKey(privkey);
- if (cacheresult) {
- return cacheresult;
- }
-
- privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey);
- const result = validateKey(privkey);
- if (result) {
- if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
- await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!);
- }
- return result;
- }
-
- /* No keysource even returned a key */
- if (!privkey) {
- throw new Error("getCrossSigningKey callback for " + type + " returned falsey");
- }
-
- /* We got some keys from the keysource, but none of them were valid */
- throw new Error("Key type " + type + " from getCrossSigningKey callback did not match");
- }
-
- /**
- * Check whether the private keys exist in secret storage.
- * XXX: This could be static, be we often seem to have an instance when we
- * want to know this anyway...
- *
- * @param secretStorage - The secret store using account data
- * @returns map of key name to key info the secret is encrypted
- * with, or null if it is not present or not encrypted with a trusted
- * key
- */
- public async isStoredInSecretStorage(
- secretStorage: SecretStorage<MatrixClient | undefined>,
- ): Promise<Record<string, object> | null> {
- // check what SSSS keys have encrypted the master key (if any)
- const stored = (await secretStorage.isStored("m.cross_signing.master")) || {};
- // then check which of those SSSS keys have also encrypted the SSK and USK
- function intersect(s: Record<string, SecretStorageKeyDescription>): void {
- for (const k of Object.keys(stored)) {
- if (!s[k]) {
- delete stored[k];
- }
- }
- }
- for (const type of ["self_signing", "user_signing"]) {
- intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
- }
- return Object.keys(stored).length ? stored : null;
- }
-
- /**
- * Store private keys in secret storage for use by other devices. This is
- * typically called in conjunction with the creation of new cross-signing
- * keys.
- *
- * @param keys - The keys to store
- * @param secretStorage - The secret store using account data
- */
- public static async storeInSecretStorage(
- keys: Map<string, Uint8Array>,
- secretStorage: SecretStorage<undefined>,
- ): Promise<void> {
- for (const [type, privateKey] of keys) {
- const encodedKey = encodeBase64(privateKey);
- await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
- }
- }
-
- /**
- * Get private keys from secret storage created by some other device. This
- * also passes the private keys to the app-specific callback.
- *
- * @param type - The type of key to get. One of "master",
- * "self_signing", or "user_signing".
- * @param secretStorage - The secret store using account data
- * @returns The private key
- */
- public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
- const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
- if (!encodedKey) {
- return null;
- }
- return decodeBase64(encodedKey);
- }
-
- /**
- * Check whether the private keys exist in the local key cache.
- *
- * @param type - The type of key to get. One of "master",
- * "self_signing", or "user_signing". Optional, will check all by default.
- * @returns True if all keys are stored in the local cache.
- */
- public async isStoredInKeyCache(type?: string): Promise<boolean> {
- const cacheCallbacks = this.cacheCallbacks;
- if (!cacheCallbacks) return false;
- const types = type ? [type] : ["master", "self_signing", "user_signing"];
- for (const t of types) {
- if (!(await cacheCallbacks.getCrossSigningKeyCache?.(t))) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Get cross-signing private keys from the local cache.
- *
- * @returns A map from key type (string) to private key (Uint8Array)
- */
- public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
- const keys = new Map();
- const cacheCallbacks = this.cacheCallbacks;
- if (!cacheCallbacks) return keys;
- for (const type of ["master", "self_signing", "user_signing"]) {
- const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type);
- if (!privKey) {
- continue;
- }
- keys.set(type, privKey);
- }
- return keys;
- }
-
- /**
- * Get the ID used to identify the user. This can also be used to test for
- * the existence of a given key type.
- *
- * @param type - The type of key to get the ID of. One of "master",
- * "self_signing", or "user_signing". Defaults to "master".
- *
- * @returns the ID
- */
- public getId(type = "master"): string | null {
- if (!this.keys[type]) return null;
- const keyInfo = this.keys[type];
- return publicKeyFromKeyInfo(keyInfo);
- }
-
- /**
- * Create new cross-signing keys for the given key types. The public keys
- * will be held in this class, while the private keys are passed off to the
- * `saveCrossSigningKeys` application callback.
- *
- * @param level - The key types to reset
- */
- public async resetKeys(level?: CrossSigningLevel): Promise<void> {
- if (!this.callbacks.saveCrossSigningKeys) {
- throw new Error("No saveCrossSigningKeys callback supplied");
- }
-
- // If we're resetting the master key, we reset all keys
- if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) {
- level = CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | CrossSigningLevel.SELF_SIGNING;
- } else if (level === (0 as CrossSigningLevel)) {
- return;
- }
-
- const privateKeys: Record<string, Uint8Array> = {};
- const keys: Record<string, ICrossSigningKey> = {};
- let masterSigning;
- let masterPub;
-
- try {
- if (level & CrossSigningLevel.MASTER) {
- masterSigning = new global.Olm.PkSigning();
- privateKeys.master = masterSigning.generate_seed();
- masterPub = masterSigning.init_with_seed(privateKeys.master);
- keys.master = {
- user_id: this.userId,
- usage: ["master"],
- keys: {
- ["ed25519:" + masterPub]: masterPub,
- },
- };
- } else {
- [masterPub, masterSigning] = await this.getCrossSigningKey("master");
- }
-
- if (level & CrossSigningLevel.SELF_SIGNING) {
- const sskSigning = new global.Olm.PkSigning();
- try {
- privateKeys.self_signing = sskSigning.generate_seed();
- const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
- keys.self_signing = {
- user_id: this.userId,
- usage: ["self_signing"],
- keys: {
- ["ed25519:" + sskPub]: sskPub,
- },
- };
- pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
- } finally {
- sskSigning.free();
- }
- }
-
- if (level & CrossSigningLevel.USER_SIGNING) {
- const uskSigning = new global.Olm.PkSigning();
- try {
- privateKeys.user_signing = uskSigning.generate_seed();
- const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
- keys.user_signing = {
- user_id: this.userId,
- usage: ["user_signing"],
- keys: {
- ["ed25519:" + uskPub]: uskPub,
- },
- };
- pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
- } finally {
- uskSigning.free();
- }
- }
-
- Object.assign(this.keys, keys);
- this.callbacks.saveCrossSigningKeys(privateKeys);
- } finally {
- if (masterSigning) {
- masterSigning.free();
- }
- }
- }
-
- /**
- * unsets the keys, used when another session has reset the keys, to disable cross-signing
- */
- public clearKeys(): void {
- this.keys = {};
- }
-
- public setKeys(keys: Record<string, ICrossSigningKey>): void {
- const signingKeys: Record<string, ICrossSigningKey> = {};
- if (keys.master) {
- if (keys.master.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- if (!this.keys.master) {
- // this is the first key we've seen, so first-use is true
- this.firstUse = true;
- } else if (publicKeyFromKeyInfo(keys.master) !== this.getId()) {
- // this is a different key, so first-use is false
- this.firstUse = false;
- } // otherwise, same key, so no change
- signingKeys.master = keys.master;
- } else if (this.keys.master) {
- signingKeys.master = this.keys.master;
- } else {
- throw new Error("Tried to set cross-signing keys without a master key");
- }
- const masterKey = publicKeyFromKeyInfo(signingKeys.master);
-
- // verify signatures
- if (keys.user_signing) {
- if (keys.user_signing.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in user_signing key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- try {
- pkVerify(keys.user_signing, masterKey, this.userId);
- } catch (e) {
- logger.error("invalid signature on user-signing key");
- // FIXME: what do we want to do here?
- throw e;
- }
- }
- if (keys.self_signing) {
- if (keys.self_signing.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in self_signing key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- try {
- pkVerify(keys.self_signing, masterKey, this.userId);
- } catch (e) {
- logger.error("invalid signature on self-signing key");
- // FIXME: what do we want to do here?
- throw e;
- }
- }
-
- // if everything checks out, then save the keys
- if (keys.master) {
- this.keys.master = keys.master;
- // if the master key is set, then the old self-signing and user-signing keys are obsolete
- delete this.keys["self_signing"];
- delete this.keys["user_signing"];
- }
- if (keys.self_signing) {
- this.keys.self_signing = keys.self_signing;
- }
- if (keys.user_signing) {
- this.keys.user_signing = keys.user_signing;
- }
- }
-
- public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void {
- // It is critical that this value latches forward from false to true but
- // never back to false to avoid a downgrade attack.
- if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
- this.crossSigningVerifiedBefore = true;
- }
- }
-
- public async signObject<T extends object>(data: T, type: string): Promise<T & { signatures: ISignatures }> {
- if (!this.keys[type]) {
- throw new Error("Attempted to sign with " + type + " key but no such key present");
- }
- const [pubkey, signing] = await this.getCrossSigningKey(type);
- try {
- pkSign(data, signing, this.userId, pubkey);
- return data as T & { signatures: ISignatures };
- } finally {
- signing.free();
- }
- }
-
- public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
- if (!this.keys.user_signing) {
- logger.info("No user signing key: not signing user");
- return;
- }
- return this.signObject(key.keys.master, "user_signing");
- }
-
- public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey | undefined> {
- if (userId !== this.userId) {
- throw new Error(`Trying to sign ${userId}'s device; can only sign our own device`);
- }
- if (!this.keys.self_signing) {
- logger.info("No self signing key: not signing device");
- return;
- }
- return this.signObject<Omit<ISignedKey, "signatures">>(
- {
- algorithms: device.algorithms,
- keys: device.keys,
- device_id: device.deviceId,
- user_id: userId,
- },
- "self_signing",
- );
- }
-
- /**
- * Check whether a given user is trusted.
- *
- * @param userCrossSigning - Cross signing info for user
- *
- * @returns
- */
- public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
- // if we're checking our own key, then it's trusted if the master key
- // and self-signing key match
- if (
- this.userId === userCrossSigning.userId &&
- this.getId() &&
- this.getId() === userCrossSigning.getId() &&
- this.getId("self_signing") &&
- this.getId("self_signing") === userCrossSigning.getId("self_signing")
- ) {
- return new UserTrustLevel(true, true, this.firstUse);
- }
-
- if (!this.keys.user_signing) {
- // If there's no user signing key, they can't possibly be verified.
- // They may be TOFU trusted though.
- return new UserTrustLevel(false, false, userCrossSigning.firstUse);
- }
-
- let userTrusted: boolean;
- const userMaster = userCrossSigning.keys.master;
- const uskId = this.getId("user_signing")!;
- try {
- pkVerify(userMaster, uskId, this.userId);
- userTrusted = true;
- } catch (e) {
- userTrusted = false;
- }
- return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse);
- }
-
- /**
- * Check whether a given device is trusted.
- *
- * @param userCrossSigning - Cross signing info for user
- * @param device - The device to check
- * @param localTrust - Whether the device is trusted locally
- * @param trustCrossSignedDevices - Whether we trust cross signed devices
- *
- * @returns
- */
- public checkDeviceTrust(
- userCrossSigning: CrossSigningInfo,
- device: DeviceInfo,
- localTrust: boolean,
- trustCrossSignedDevices: boolean,
- ): DeviceTrustLevel {
- const userTrust = this.checkUserTrust(userCrossSigning);
-
- const userSSK = userCrossSigning.keys.self_signing;
- if (!userSSK) {
- // if the user has no self-signing key then we cannot make any
- // trust assertions about this device from cross-signing
- return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
- }
-
- const deviceObj = deviceToObject(device, userCrossSigning.userId);
- try {
- // if we can verify the user's SSK from their master key...
- pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId);
- // ...and this device's key from their SSK...
- pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
- // ...then we trust this device as much as far as we trust the user
- return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
- } catch (e) {
- return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
- }
- }
-
- /**
- * @returns Cache callbacks
- */
- public getCacheCallbacks(): ICacheCallbacks {
- return this.cacheCallbacks;
- }
-}
-
-interface DeviceObject extends IObject {
- algorithms: string[];
- keys: Record<string, string>;
- device_id: string;
- user_id: string;
-}
-
-function deviceToObject(device: DeviceInfo, userId: string): DeviceObject {
- return {
- algorithms: device.algorithms,
- keys: device.keys,
- device_id: device.deviceId,
- user_id: userId,
- signatures: device.signatures,
- };
-}
-
-export enum CrossSigningLevel {
- MASTER = 4,
- USER_SIGNING = 2,
- SELF_SIGNING = 1,
-}
-
-/**
- * Represents the ways in which we trust a user
- */
-export class UserTrustLevel {
- public constructor(
- private readonly crossSigningVerified: boolean,
- private readonly crossSigningVerifiedBefore: boolean,
- private readonly tofu: boolean,
- ) {}
-
- /**
- * @returns true if this user is verified via any means
- */
- public isVerified(): boolean {
- return this.isCrossSigningVerified();
- }
-
- /**
- * @returns true if this user is verified via cross signing
- */
- public isCrossSigningVerified(): boolean {
- return this.crossSigningVerified;
- }
-
- /**
- * @returns true if we ever verified this user before (at least for
- * the history of verifications observed by this device).
- */
- public wasCrossSigningVerified(): boolean {
- return this.crossSigningVerifiedBefore;
- }
-
- /**
- * @returns true if this user's key is trusted on first use
- */
- public isTofu(): boolean {
- return this.tofu;
- }
-}
-
-/**
- * Represents the ways in which we trust a device
- */
-export class DeviceTrustLevel {
- public constructor(
- public readonly crossSigningVerified: boolean,
- public readonly tofu: boolean,
- private readonly localVerified: boolean,
- private readonly trustCrossSignedDevices: boolean,
- ) {}
-
- public static fromUserTrustLevel(
- userTrustLevel: UserTrustLevel,
- localVerified: boolean,
- trustCrossSignedDevices: boolean,
- ): DeviceTrustLevel {
- return new DeviceTrustLevel(
- userTrustLevel.isCrossSigningVerified(),
- userTrustLevel.isTofu(),
- localVerified,
- trustCrossSignedDevices,
- );
- }
-
- /**
- * @returns true if this device is verified via any means
- */
- public isVerified(): boolean {
- return Boolean(this.isLocallyVerified() || (this.trustCrossSignedDevices && this.isCrossSigningVerified()));
- }
-
- /**
- * @returns true if this device is verified via cross signing
- */
- public isCrossSigningVerified(): boolean {
- return this.crossSigningVerified;
- }
-
- /**
- * @returns true if this device is verified locally
- */
- public isLocallyVerified(): boolean {
- return this.localVerified;
- }
-
- /**
- * @returns true if this device is trusted from a user's key
- * that is trusted on first use
- */
- public isTofu(): boolean {
- return this.tofu;
- }
-}
-
-export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
- return {
- getCrossSigningKeyCache: async function (
- type: keyof SecretStorePrivateKeys,
- _expectedPublicKey: string,
- ): Promise<Uint8Array> {
- const key = await new Promise<any>((resolve) => {
- return store.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- store.getSecretStorePrivateKey(txn, resolve, type);
- });
- });
-
- if (key && key.ciphertext) {
- const pickleKey = Buffer.from(olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, type);
- return decodeBase64(decrypted);
- } else {
- return key;
- }
- },
- storeCrossSigningKeyCache: async function (
- type: keyof SecretStorePrivateKeys,
- key?: Uint8Array,
- ): Promise<void> {
- if (!(key instanceof Uint8Array)) {
- throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
- }
- const pickleKey = Buffer.from(olmDevice.pickleKey);
- const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
- return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- store.storeSecretStorePrivateKey(txn, type, encryptedKey);
- });
- },
- };
-}
-
-export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
-
-/**
- * Request cross-signing keys from another device during verification.
- *
- * @param baseApis - base Matrix API interface
- * @param userId - The user ID being verified
- * @param deviceId - The device ID being verified
- */
-export async function requestKeysDuringVerification(
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
-): Promise<KeysDuringVerification | void> {
- // If this is a self-verification, ask the other party for keys
- if (baseApis.getUserId() !== userId) {
- return;
- }
- logger.log("Cross-signing: Self-verification done; requesting keys");
- // This happens asynchronously, and we're not concerned about waiting for
- // it. We return here in order to test.
- return new Promise<KeysDuringVerification | void>((resolve, reject) => {
- const client = baseApis;
- const original = client.crypto!.crossSigningInfo;
-
- // We already have all of the infrastructure we need to validate and
- // cache cross-signing keys, so instead of replicating that, here we set
- // up callbacks that request them from the other device and call
- // CrossSigningInfo.getCrossSigningKey() to validate/cache
- const crossSigning = new CrossSigningInfo(
- original.userId,
- {
- getCrossSigningKey: async (type): Promise<Uint8Array> => {
- logger.debug("Cross-signing: requesting secret", type, deviceId);
- const { promise } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]);
- const result = await promise;
- const decoded = decodeBase64(result);
- return Uint8Array.from(decoded);
- },
- },
- original.getCacheCallbacks(),
- );
- crossSigning.keys = original.keys;
-
- // XXX: get all keys out if we get one key out
- // https://github.com/vector-im/element-web/issues/12604
- // then change here to reject on the timeout
- // Requests can be ignored, so don't wait around forever
- const timeout = new Promise<void>((resolve) => {
- setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout"));
- });
-
- // also request and cache the key backup key
- const backupKeyPromise = (async (): Promise<void> => {
- const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
- if (!cachedKey) {
- logger.info("No cached backup key found. Requesting...");
- const secretReq = client.requestSecret("m.megolm_backup.v1", [deviceId]);
- const base64Key = await secretReq.promise;
- logger.info("Got key backup key, decoding...");
- const decodedKey = decodeBase64(base64Key);
- logger.info("Decoded backup key, storing...");
- await client.crypto!.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey));
- logger.info("Backup key stored. Starting backup restore...");
- const backupInfo = await client.getKeyBackupVersion();
- // no need to await for this - just let it go in the bg
- client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => {
- logger.info("Backup restored.");
- });
- }
- })();
-
- // We call getCrossSigningKey() for its side-effects
- return Promise.race<KeysDuringVerification | void>([
- Promise.all([
- crossSigning.getCrossSigningKey("master"),
- crossSigning.getCrossSigningKey("self_signing"),
- crossSigning.getCrossSigningKey("user_signing"),
- backupKeyPromise,
- ]) as Promise<KeysDuringVerification>,
- timeout,
- ]).then(resolve, reject);
- }).catch((e) => {
- logger.warn("Cross-signing: failure while requesting keys:", e);
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts
deleted file mode 100644
index a1ff0eb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts
+++ /dev/null
@@ -1,989 +0,0 @@
-/*
-Copyright 2017 - 2021 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.
-*/
-
-/**
- * Manages the list of other users' devices
- */
-
-import { logger } from "../logger";
-import { DeviceInfo, IDevice } from "./deviceinfo";
-import { CrossSigningInfo, ICrossSigningInfo } from "./CrossSigning";
-import * as olmlib from "./olmlib";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { chunkPromises, defer, IDeferred, sleep } from "../utils";
-import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client";
-import { OlmDevice } from "./OlmDevice";
-import { CryptoStore } from "./store/base";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { CryptoEvent, CryptoEventHandlerMap } from "./index";
-
-/* State transition diagram for DeviceList.deviceTrackingStatus
- *
- * |
- * stopTrackingDeviceList V
- * +---------------------> NOT_TRACKED
- * | |
- * +<--------------------+ | startTrackingDeviceList
- * | | V
- * | +-------------> PENDING_DOWNLOAD <--------------------+-+
- * | | ^ | | |
- * | | restart download | | start download | | invalidateUserDeviceList
- * | | client failed | | | |
- * | | | V | |
- * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
- * | | | |
- * +<-------------------+ | download successful |
- * ^ V |
- * +----------------------- UP_TO_DATE ------------------------+
- */
-
-// constants for DeviceList.deviceTrackingStatus
-export enum TrackingStatus {
- NotTracked,
- PendingDownload,
- DownloadInProgress,
- UpToDate,
-}
-
-// user-Id → device-Id → DeviceInfo
-export type DeviceInfoMap = Map<string, Map<string, DeviceInfo>>;
-
-type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
-
-export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
- private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
-
- public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
-
- // map of identity keys to the user who owns it
- private userByIdentityKey: Record<string, string> = {};
-
- // which users we are tracking device status for.
- private deviceTrackingStatus: { [userId: string]: TrackingStatus } = {}; // loaded from storage in load()
-
- // The 'next_batch' sync token at the point the data was written,
- // ie. a token representing the point immediately after the
- // moment represented by the snapshot in the db.
- private syncToken: string | null = null;
-
- private keyDownloadsInProgressByUser = new Map<string, Promise<void>>();
-
- // Set whenever changes are made other than setting the sync token
- private dirty = false;
-
- // Promise resolved when device data is saved
- private savePromise: Promise<boolean> | null = null;
- // Function that resolves the save promise
- private resolveSavePromise: ((saved: boolean) => void) | null = null;
- // The time the save is scheduled for
- private savePromiseTime: number | null = null;
- // The timer used to delay the save
- private saveTimer: ReturnType<typeof setTimeout> | null = null;
- // True if we have fetched data from the server or loaded a non-empty
- // set of device data from the store
- private hasFetched: boolean | null = null;
-
- private readonly serialiser: DeviceListUpdateSerialiser;
-
- public constructor(
- baseApis: MatrixClient,
- private readonly cryptoStore: CryptoStore,
- olmDevice: OlmDevice,
- // Maximum number of user IDs per request to prevent server overload (#1619)
- public readonly keyDownloadChunkSize = 250,
- ) {
- super();
-
- this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
- }
-
- /**
- * Load the device tracking state from storage
- */
- public async load(): Promise<void> {
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
- this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
- this.hasFetched = Boolean(deviceData && deviceData.devices);
- this.devices = deviceData ? deviceData.devices : {};
- this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {};
- this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {};
- this.syncToken = deviceData?.syncToken ?? null;
- this.userByIdentityKey = {};
- for (const user of Object.keys(this.devices)) {
- const userDevices = this.devices[user];
- for (const device of Object.keys(userDevices)) {
- const idKey = userDevices[device].keys["curve25519:" + device];
- if (idKey !== undefined) {
- this.userByIdentityKey[idKey] = user;
- }
- }
- }
- });
- });
-
- for (const u of Object.keys(this.deviceTrackingStatus)) {
- // if a download was in progress when we got shut down, it isn't any more.
- if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) {
- this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
- }
- }
- }
-
- public stop(): void {
- if (this.saveTimer !== null) {
- clearTimeout(this.saveTimer);
- }
- }
-
- /**
- * Save the device tracking state to storage, if any changes are
- * pending other than updating the sync token
- *
- * The actual save will be delayed by a short amount of time to
- * aggregate multiple writes to the database.
- *
- * @param delay - Time in ms before which the save actually happens.
- * By default, the save is delayed for a short period in order to batch
- * multiple writes, but this behaviour can be disabled by passing 0.
- *
- * @returns true if the data was saved, false if
- * it was not (eg. because no changes were pending). The promise
- * will only resolve once the data is saved, so may take some time
- * to resolve.
- */
- public async saveIfDirty(delay = 500): Promise<boolean> {
- if (!this.dirty) return Promise.resolve(false);
- // Delay saves for a bit so we can aggregate multiple saves that happen
- // in quick succession (eg. when a whole room's devices are marked as known)
-
- const targetTime = Date.now() + delay;
- if (this.savePromiseTime && targetTime < this.savePromiseTime) {
- // There's a save scheduled but for after we would like: cancel
- // it & schedule one for the time we want
- clearTimeout(this.saveTimer!);
- this.saveTimer = null;
- this.savePromiseTime = null;
- // (but keep the save promise since whatever called save before
- // will still want to know when the save is done)
- }
-
- let savePromise = this.savePromise;
- if (savePromise === null) {
- savePromise = new Promise((resolve) => {
- this.resolveSavePromise = resolve;
- });
- this.savePromise = savePromise;
- }
-
- if (this.saveTimer === null) {
- const resolveSavePromise = this.resolveSavePromise;
- this.savePromiseTime = targetTime;
- this.saveTimer = setTimeout(() => {
- logger.log("Saving device tracking data", this.syncToken);
-
- // null out savePromise now (after the delay but before the write),
- // otherwise we could return the existing promise when the save has
- // actually already happened.
- this.savePromiseTime = null;
- this.saveTimer = null;
- this.savePromise = null;
- this.resolveSavePromise = null;
-
- this.cryptoStore
- .doTxn("readwrite", [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
- this.cryptoStore.storeEndToEndDeviceData(
- {
- devices: this.devices,
- crossSigningInfo: this.crossSigningInfo,
- trackingStatus: this.deviceTrackingStatus,
- syncToken: this.syncToken ?? undefined,
- },
- txn,
- );
- })
- .then(
- () => {
- // The device list is considered dirty until the write completes.
- this.dirty = false;
- resolveSavePromise?.(true);
- },
- (err) => {
- logger.error("Failed to save device tracking data", this.syncToken);
- logger.error(err);
- },
- );
- }, delay);
- }
-
- return savePromise;
- }
-
- /**
- * Gets the sync token last set with setSyncToken
- *
- * @returns The sync token
- */
- public getSyncToken(): string | null {
- return this.syncToken;
- }
-
- /**
- * Sets the sync token that the app will pass as the 'since' to the /sync
- * endpoint next time it syncs.
- * The sync token must always be set after any changes made as a result of
- * data in that sync since setting the sync token to a newer one will mean
- * those changed will not be synced from the server if a new client starts
- * up with that data.
- *
- * @param st - The sync token
- */
- public setSyncToken(st: string | null): void {
- this.syncToken = st;
- }
-
- /**
- * Ensures up to date keys for a list of users are stored in the session store,
- * downloading and storing them if they're not (or if forceDownload is
- * true).
- * @param userIds - The users to fetch.
- * @param forceDownload - Always download the keys even if cached.
- *
- * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}.
- */
- public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
- const usersToDownload: string[] = [];
- const promises: Promise<unknown>[] = [];
-
- userIds.forEach((u) => {
- const trackingStatus = this.deviceTrackingStatus[u];
- if (this.keyDownloadsInProgressByUser.has(u)) {
- // already a key download in progress/queued for this user; its results
- // will be good enough for us.
- logger.log(`downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`);
- promises.push(this.keyDownloadsInProgressByUser.get(u)!);
- } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
- usersToDownload.push(u);
- }
- });
-
- if (usersToDownload.length != 0) {
- logger.log("downloadKeys: downloading for", usersToDownload);
- const downloadPromise = this.doKeyDownload(usersToDownload);
- promises.push(downloadPromise);
- }
-
- if (promises.length === 0) {
- logger.log("downloadKeys: already have all necessary keys");
- }
-
- return Promise.all(promises).then(() => {
- return this.getDevicesFromStore(userIds);
- });
- }
-
- /**
- * Get the stored device keys for a list of user ids
- *
- * @param userIds - the list of users to list keys for.
- *
- * @returns userId-\>deviceId-\>{@link DeviceInfo}.
- */
- private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
- const stored: DeviceInfoMap = new Map();
- userIds.forEach((userId) => {
- const deviceMap = new Map();
- this.getStoredDevicesForUser(userId)?.forEach(function (device) {
- deviceMap.set(device.deviceId, device);
- });
- stored.set(userId, deviceMap);
- });
- return stored;
- }
-
- /**
- * Returns a list of all user IDs the DeviceList knows about
- *
- * @returns All known user IDs
- */
- public getKnownUserIds(): string[] {
- return Object.keys(this.devices);
- }
-
- /**
- * Get the stored device keys for a user id
- *
- * @param userId - the user to list keys for.
- *
- * @returns list of devices, or null if we haven't
- * managed to get a list of devices for this user yet.
- */
- public getStoredDevicesForUser(userId: string): DeviceInfo[] | null {
- const devs = this.devices[userId];
- if (!devs) {
- return null;
- }
- const res: DeviceInfo[] = [];
- for (const deviceId in devs) {
- if (devs.hasOwnProperty(deviceId)) {
- res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
- }
- }
- return res;
- }
-
- /**
- * Get the stored device data for a user, in raw object form
- *
- * @param userId - the user to get data for
- *
- * @returns `deviceId->{object}` devices, or undefined if
- * there is no data for this user.
- */
- public getRawStoredDevicesForUser(userId: string): Record<string, IDevice> {
- return this.devices[userId];
- }
-
- public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
- if (!this.crossSigningInfo[userId]) return null;
-
- return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
- }
-
- public storeCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
- this.crossSigningInfo[userId] = info;
- this.dirty = true;
- }
-
- /**
- * Get the stored keys for a single device
- *
- *
- * @returns device, or undefined
- * if we don't know about this device
- */
- public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
- const devs = this.devices[userId];
- if (!devs?.[deviceId]) {
- return undefined;
- }
- return DeviceInfo.fromStorage(devs[deviceId], deviceId);
- }
-
- /**
- * Get a user ID by one of their device's curve25519 identity key
- *
- * @param algorithm - encryption algorithm
- * @param senderKey - curve25519 key to match
- *
- * @returns user ID
- */
- public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
- if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
- // we only deal in olm keys
- return null;
- }
-
- return this.userByIdentityKey[senderKey];
- }
-
- /**
- * Find a device by curve25519 identity key
- *
- * @param algorithm - encryption algorithm
- * @param senderKey - curve25519 key to match
- */
- public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null {
- const userId = this.getUserByIdentityKey(algorithm, senderKey);
- if (!userId) {
- return null;
- }
-
- const devices = this.devices[userId];
- if (!devices) {
- return null;
- }
-
- for (const deviceId in devices) {
- if (!devices.hasOwnProperty(deviceId)) {
- continue;
- }
-
- const device = devices[deviceId];
- for (const keyId in device.keys) {
- if (!device.keys.hasOwnProperty(keyId)) {
- continue;
- }
- if (keyId.indexOf("curve25519:") !== 0) {
- continue;
- }
- const deviceKey = device.keys[keyId];
- if (deviceKey == senderKey) {
- return DeviceInfo.fromStorage(device, deviceId);
- }
- }
- }
-
- // doesn't match a known device
- return null;
- }
-
- /**
- * Replaces the list of devices for a user with the given device list
- *
- * @param userId - The user ID
- * @param devices - New device info for user
- */
- public storeDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
- this.setRawStoredDevicesForUser(userId, devices);
- this.dirty = true;
- }
-
- /**
- * flag the given user for device-list tracking, if they are not already.
- *
- * This will mean that a subsequent call to refreshOutdatedDeviceLists()
- * will download the device list for the user, and that subsequent calls to
- * invalidateUserDeviceList will trigger more updates.
- *
- */
- public startTrackingDeviceList(userId: string): void {
- // sanity-check the userId. This is mostly paranoia, but if synapse
- // can't parse the userId we give it as an mxid, it 500s the whole
- // request and we can never update the device lists again (because
- // the broken userId is always 'invalid' and always included in any
- // refresh request).
- // By checking it is at least a string, we can eliminate a class of
- // silly errors.
- if (typeof userId !== "string") {
- throw new Error("userId must be a string; was " + userId);
- }
- if (!this.deviceTrackingStatus[userId]) {
- logger.log("Now tracking device list for " + userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * Mark the given user as no longer being tracked for device-list updates.
- *
- * This won't affect any in-progress downloads, which will still go on to
- * complete; it will just mean that we don't think that we have an up-to-date
- * list for future calls to downloadKeys.
- *
- */
- public stopTrackingDeviceList(userId: string): void {
- if (this.deviceTrackingStatus[userId]) {
- logger.log("No longer tracking device list for " + userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
-
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * Set all users we're currently tracking to untracked
- *
- * This will flag each user whose devices we are tracking as in need of an
- * update.
- */
- public stopTrackingAllDeviceLists(): void {
- for (const userId of Object.keys(this.deviceTrackingStatus)) {
- this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
- }
- this.dirty = true;
- }
-
- /**
- * Mark the cached device list for the given user outdated.
- *
- * If we are not tracking this user's devices, we'll do nothing. Otherwise
- * we flag the user as needing an update.
- *
- * This doesn't actually set off an update, so that several users can be
- * batched together. Call refreshOutdatedDeviceLists() for that.
- *
- */
- public invalidateUserDeviceList(userId: string): void {
- if (this.deviceTrackingStatus[userId]) {
- logger.log("Marking device list outdated for", userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
-
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * If we have users who have outdated device lists, start key downloads for them
- *
- * @returns which completes when the download completes; normally there
- * is no need to wait for this (it's mostly for the unit tests).
- */
- public refreshOutdatedDeviceLists(): Promise<void> {
- this.saveIfDirty();
-
- const usersToDownload: string[] = [];
- for (const userId of Object.keys(this.deviceTrackingStatus)) {
- const stat = this.deviceTrackingStatus[userId];
- if (stat == TrackingStatus.PendingDownload) {
- usersToDownload.push(userId);
- }
- }
-
- return this.doKeyDownload(usersToDownload);
- }
-
- /**
- * Set the stored device data for a user, in raw object form
- * Used only by internal class DeviceListUpdateSerialiser
- *
- * @param userId - the user to get data for
- *
- * @param devices - `deviceId->{object}` the new devices
- */
- public setRawStoredDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
- // remove old devices from userByIdentityKey
- if (this.devices[userId] !== undefined) {
- for (const [deviceId, dev] of Object.entries(this.devices[userId])) {
- const identityKey = dev.keys["curve25519:" + deviceId];
-
- delete this.userByIdentityKey[identityKey];
- }
- }
-
- this.devices[userId] = devices;
-
- // add new devices into userByIdentityKey
- for (const [deviceId, dev] of Object.entries(devices)) {
- const identityKey = dev.keys["curve25519:" + deviceId];
-
- this.userByIdentityKey[identityKey] = userId;
- }
- }
-
- public setRawStoredCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
- this.crossSigningInfo[userId] = info;
- }
-
- /**
- * Fire off download update requests for the given users, and update the
- * device list tracking status for them, and the
- * keyDownloadsInProgressByUser map for them.
- *
- * @param users - list of userIds
- *
- * @returns resolves when all the users listed have
- * been updated. rejects if there was a problem updating any of the
- * users.
- */
- private doKeyDownload(users: string[]): Promise<void> {
- if (users.length === 0) {
- // nothing to do
- return Promise.resolve();
- }
-
- const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(
- () => {
- finished(true);
- },
- (e) => {
- logger.error("Error downloading keys for " + users + ":", e);
- finished(false);
- throw e;
- },
- );
-
- users.forEach((u) => {
- this.keyDownloadsInProgressByUser.set(u, prom);
- const stat = this.deviceTrackingStatus[u];
- if (stat == TrackingStatus.PendingDownload) {
- this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
- }
- });
-
- const finished = (success: boolean): void => {
- this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
- users.forEach((u) => {
- this.dirty = true;
-
- // we may have queued up another download request for this user
- // since we started this request. If that happens, we should
- // ignore the completion of the first one.
- if (this.keyDownloadsInProgressByUser.get(u) !== prom) {
- logger.log("Another update in the queue for", u, "- not marking up-to-date");
- return;
- }
- this.keyDownloadsInProgressByUser.delete(u);
- const stat = this.deviceTrackingStatus[u];
- if (stat == TrackingStatus.DownloadInProgress) {
- if (success) {
- // we didn't get any new invalidations since this download started:
- // this user's device list is now up to date.
- this.deviceTrackingStatus[u] = TrackingStatus.UpToDate;
- logger.log("Device list for", u, "now up to date");
- } else {
- this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
- }
- }
- });
- this.saveIfDirty();
- this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
- this.hasFetched = true;
- };
-
- return prom;
- }
-}
-
-/**
- * Serialises updates to device lists
- *
- * Ensures that results from /keys/query are not overwritten if a second call
- * completes *before* an earlier one.
- *
- * It currently does this by ensuring only one call to /keys/query happens at a
- * time (and queuing other requests up).
- */
-class DeviceListUpdateSerialiser {
- private downloadInProgress = false;
-
- // users which are queued for download
- // userId -> true
- private keyDownloadsQueuedByUser: Record<string, boolean> = {};
-
- // deferred which is resolved when the queued users are downloaded.
- // non-null indicates that we have users queued for download.
- private queuedQueryDeferred?: IDeferred<void>;
-
- private syncToken?: string; // The sync token we send with the requests
-
- /*
- * @param baseApis - Base API object
- * @param olmDevice - The Olm Device
- * @param deviceList - The device list object, the device list to be updated
- */
- public constructor(
- private readonly baseApis: MatrixClient,
- private readonly olmDevice: OlmDevice,
- private readonly deviceList: DeviceList,
- ) {}
-
- /**
- * Make a key query request for the given users
- *
- * @param users - list of user ids
- *
- * @param syncToken - sync token to pass in the query request, to
- * help the HS give the most recent results
- *
- * @returns resolves when all the users listed have
- * been updated. rejects if there was a problem updating any of the
- * users.
- */
- public updateDevicesForUsers(users: string[], syncToken: string): Promise<void> {
- users.forEach((u) => {
- this.keyDownloadsQueuedByUser[u] = true;
- });
-
- if (!this.queuedQueryDeferred) {
- this.queuedQueryDeferred = defer();
- }
-
- // We always take the new sync token and just use the latest one we've
- // been given, since it just needs to be at least as recent as the
- // sync response the device invalidation message arrived in
- this.syncToken = syncToken;
-
- if (this.downloadInProgress) {
- // just queue up these users
- logger.log("Queued key download for", users);
- return this.queuedQueryDeferred.promise;
- }
-
- // start a new download.
- return this.doQueuedQueries();
- }
-
- private doQueuedQueries(): Promise<void> {
- if (this.downloadInProgress) {
- throw new Error("DeviceListUpdateSerialiser.doQueuedQueries called with request active");
- }
-
- const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
- this.keyDownloadsQueuedByUser = {};
- const deferred = this.queuedQueryDeferred;
- this.queuedQueryDeferred = undefined;
-
- logger.log("Starting key download for", downloadUsers);
- this.downloadInProgress = true;
-
- const opts: Parameters<MatrixClient["downloadKeysForUsers"]>[1] = {};
- if (this.syncToken) {
- opts.token = this.syncToken;
- }
-
- const factories: Array<() => Promise<IDownloadKeyResult>> = [];
- for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
- const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
- factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
- }
-
- chunkPromises(factories, 3)
- .then(async (responses: IDownloadKeyResult[]) => {
- const dk: IDownloadKeyResult["device_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.device_keys || {}),
- );
- const masterKeys: IDownloadKeyResult["master_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.master_keys || {}),
- );
- const ssks: IDownloadKeyResult["self_signing_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.self_signing_keys || {}),
- );
- const usks: IDownloadKeyResult["user_signing_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.user_signing_keys || {}),
- );
-
- // yield to other things that want to execute in between users, to
- // avoid wedging the CPU
- // (https://github.com/vector-im/element-web/issues/3158)
- //
- // of course we ought to do this in a web worker or similar, but
- // this serves as an easy solution for now.
- for (const userId of downloadUsers) {
- await sleep(5);
- try {
- await this.processQueryResponseForUser(userId, dk[userId], {
- master: masterKeys?.[userId],
- self_signing: ssks?.[userId],
- user_signing: usks?.[userId],
- });
- } catch (e) {
- // log the error but continue, so that one bad key
- // doesn't kill the whole process
- logger.error(`Error processing keys for ${userId}:`, e);
- }
- }
- })
- .then(
- () => {
- logger.log("Completed key download for " + downloadUsers);
-
- this.downloadInProgress = false;
- deferred?.resolve();
-
- // if we have queued users, fire off another request.
- if (this.queuedQueryDeferred) {
- this.doQueuedQueries();
- }
- },
- (e) => {
- logger.warn("Error downloading keys for " + downloadUsers + ":", e);
- this.downloadInProgress = false;
- deferred?.reject(e);
- },
- );
-
- return deferred!.promise;
- }
-
- private async processQueryResponseForUser(
- userId: string,
- dkResponse: DeviceKeys,
- crossSigningResponse: {
- master?: Keys;
- self_signing?: SigningKeys;
- user_signing?: SigningKeys;
- },
- ): Promise<void> {
- logger.log("got device keys for " + userId + ":", dkResponse);
- logger.log("got cross-signing keys for " + userId + ":", crossSigningResponse);
-
- {
- // map from deviceid -> deviceinfo for this user
- const userStore: Record<string, DeviceInfo> = {};
- const devs = this.deviceList.getRawStoredDevicesForUser(userId);
- if (devs) {
- Object.keys(devs).forEach((deviceId) => {
- const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
- userStore[deviceId] = d;
- });
- }
-
- await updateStoredDeviceKeysForUser(
- this.olmDevice,
- userId,
- userStore,
- dkResponse || {},
- this.baseApis.getUserId()!,
- this.baseApis.deviceId!,
- );
-
- // put the updates into the object that will be returned as our results
- const storage: Record<string, IDevice> = {};
- Object.keys(userStore).forEach((deviceId) => {
- storage[deviceId] = userStore[deviceId].toStorage();
- });
-
- this.deviceList.setRawStoredDevicesForUser(userId, storage);
- }
-
- // now do the same for the cross-signing keys
- {
- // FIXME: should we be ignoring empty cross-signing responses, or
- // should we be dropping the keys?
- if (
- crossSigningResponse &&
- (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)
- ) {
- const crossSigning =
- this.deviceList.getStoredCrossSigningForUser(userId) || new CrossSigningInfo(userId);
-
- crossSigning.setKeys(crossSigningResponse);
-
- this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
-
- // NB. Unlike most events in the js-sdk, this one is internal to the
- // js-sdk and is not re-emitted
- this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
- }
- }
- }
-}
-
-async function updateStoredDeviceKeysForUser(
- olmDevice: OlmDevice,
- userId: string,
- userStore: Record<string, DeviceInfo>,
- userResult: IDownloadKeyResult["device_keys"]["user_id"],
- localUserId: string,
- localDeviceId: string,
-): Promise<boolean> {
- let updated = false;
-
- // remove any devices in the store which aren't in the response
- for (const deviceId in userStore) {
- if (!userStore.hasOwnProperty(deviceId)) {
- continue;
- }
-
- if (!(deviceId in userResult)) {
- if (userId === localUserId && deviceId === localDeviceId) {
- logger.warn(`Local device ${deviceId} missing from sync, skipping removal`);
- continue;
- }
-
- logger.log("Device " + userId + ":" + deviceId + " has been removed");
- delete userStore[deviceId];
- updated = true;
- }
- }
-
- for (const deviceId in userResult) {
- if (!userResult.hasOwnProperty(deviceId)) {
- continue;
- }
-
- const deviceResult = userResult[deviceId];
-
- // check that the user_id and device_id in the response object are
- // correct
- if (deviceResult.user_id !== userId) {
- logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId);
- continue;
- }
- if (deviceResult.device_id !== deviceId) {
- logger.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + deviceId);
- continue;
- }
-
- if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) {
- updated = true;
- }
- }
-
- return updated;
-}
-
-/*
- * Process a device in a /query response, and add it to the userStore
- *
- * returns (a promise for) true if a change was made, else false
- */
-async function storeDeviceKeys(
- olmDevice: OlmDevice,
- userStore: Record<string, DeviceInfo>,
- deviceResult: IDownloadKeyResult["device_keys"]["user_id"]["device_id"],
-): Promise<boolean> {
- if (!deviceResult.keys) {
- // no keys?
- return false;
- }
-
- const deviceId = deviceResult.device_id;
- const userId = deviceResult.user_id;
-
- const signKeyId = "ed25519:" + deviceId;
- const signKey = deviceResult.keys[signKeyId];
- if (!signKey) {
- logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key");
- return false;
- }
-
- const unsigned = deviceResult.unsigned || {};
- const signatures = deviceResult.signatures || {};
-
- try {
- await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey);
- } catch (e) {
- logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e);
- return false;
- }
-
- // DeviceInfo
- let deviceStore;
-
- if (deviceId in userStore) {
- // already have this device.
- deviceStore = userStore[deviceId];
-
- if (deviceStore.getFingerprint() != signKey) {
- // this should only happen if the list has been MITMed; we are
- // best off sticking with the original keys.
- //
- // Should we warn the user about it somehow?
- logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed");
- return false;
- }
- } else {
- userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
- }
-
- deviceStore.keys = deviceResult.keys || {};
- deviceStore.algorithms = deviceResult.algorithms || [];
- deviceStore.unsigned = unsigned;
- deviceStore.signatures = signatures;
- return true;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts
deleted file mode 100644
index 4efe677..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
-Copyright 2021 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 { logger } from "../logger";
-import { IContent, MatrixEvent } from "../models/event";
-import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { Method, ClientPrefix } from "../http-api";
-import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
-import {
- ClientEvent,
- ClientEventHandlerMap,
- CrossSigningKeys,
- ICrossSigningKey,
- ISignedKey,
- KeySignatures,
-} from "../client";
-import { IKeyBackupInfo } from "./keybackup";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { IAccountDataClient } from "./SecretStorage";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-interface ICrossSigningKeys {
- authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
- keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
-}
-
-/**
- * Builds an EncryptionSetupOperation by calling any of the add.. methods.
- * Once done, `buildOperation()` can be called which allows to apply to operation.
- *
- * This is used as a helper by Crypto to keep track of all the network requests
- * and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
- * Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
- * more than once.
- */
-export class EncryptionSetupBuilder {
- public readonly accountDataClientAdapter: AccountDataClientAdapter;
- public readonly crossSigningCallbacks: CrossSigningCallbacks;
- public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
-
- private crossSigningKeys?: ICrossSigningKeys;
- private keySignatures?: KeySignatures;
- private keyBackupInfo?: IKeyBackupInfo;
- private sessionBackupPrivateKey?: Uint8Array;
-
- /**
- * @param accountData - pre-existing account data, will only be read, not written.
- * @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet
- */
- public constructor(accountData: Map<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
- this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
- this.crossSigningCallbacks = new CrossSigningCallbacks();
- this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
- }
-
- /**
- * Adds new cross-signing public keys
- *
- * @param authUpload - Function called to await an interactive auth
- * flow when uploading device signing keys.
- * Args:
- * A function that makes the request requiring auth. Receives
- * the auth data as an object. Can be called multiple times, first with
- * an empty authDict, to obtain the flows.
- * @param keys - the new keys
- */
- public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void {
- this.crossSigningKeys = { authUpload, keys };
- }
-
- /**
- * Adds the key backup info to be updated on the server
- *
- * Used either to create a new key backup, or add signatures
- * from the new MSK.
- *
- * @param keyBackupInfo - as received from/sent to the server
- */
- public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void {
- this.keyBackupInfo = keyBackupInfo;
- }
-
- /**
- * Adds the session backup private key to be updated in the local cache
- *
- * Used after fixing the format of the key
- *
- */
- public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void {
- this.sessionBackupPrivateKey = privateKey;
- }
-
- /**
- * Add signatures from a given user and device/x-sign key
- * Used to sign the new cross-signing key with the device key
- *
- */
- public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void {
- if (!this.keySignatures) {
- this.keySignatures = {};
- }
- const userSignatures = this.keySignatures[userId] || {};
- this.keySignatures[userId] = userSignatures;
- userSignatures[deviceId] = signature;
- }
-
- public async setAccountData(type: string, content: object): Promise<void> {
- await this.accountDataClientAdapter.setAccountData(type, content);
- }
-
- /**
- * builds the operation containing all the parts that have been added to the builder
- */
- public buildOperation(): EncryptionSetupOperation {
- const accountData = this.accountDataClientAdapter.values;
- return new EncryptionSetupOperation(accountData, this.crossSigningKeys, this.keyBackupInfo, this.keySignatures);
- }
-
- /**
- * Stores the created keys locally.
- *
- * This does not yet store the operation in a way that it can be restored,
- * but that is the idea in the future.
- */
- public async persist(crypto: Crypto): Promise<void> {
- // store private keys in cache
- if (this.crossSigningKeys) {
- const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice);
- for (const type of ["master", "self_signing", "user_signing"]) {
- logger.log(`Cache ${type} cross-signing private key locally`);
- const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
- await cacheCallbacks.storeCrossSigningKeyCache?.(type, privateKey);
- }
- // store own cross-sign pubkeys as trusted
- await crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- crypto.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningKeys!.keys);
- });
- }
- // store session backup key in cache
- if (this.sessionBackupPrivateKey) {
- await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey);
- }
- }
-}
-
-/**
- * Can be created from EncryptionSetupBuilder, or
- * (in a follow-up PR, not implemented yet) restored from storage, to retry.
- *
- * It does not have knowledge of any private keys, unlike the builder.
- */
-export class EncryptionSetupOperation {
- /**
- */
- public constructor(
- private readonly accountData: Map<string, object>,
- private readonly crossSigningKeys?: ICrossSigningKeys,
- private readonly keyBackupInfo?: IKeyBackupInfo,
- private readonly keySignatures?: KeySignatures,
- ) {}
-
- /**
- * Runs the (remaining part of, in the future) operation by sending requests to the server.
- */
- public async apply(crypto: Crypto): Promise<void> {
- const baseApis = crypto.baseApis;
- // upload cross-signing keys
- if (this.crossSigningKeys) {
- const keys: Partial<CrossSigningKeys> = {};
- for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
- keys[((name as keyof ICrossSigningKeys["keys"]) + "_key") as keyof CrossSigningKeys] = key;
- }
-
- // We must only call `uploadDeviceSigningKeys` from inside this auth
- // helper to ensure we properly handle auth errors.
- await this.crossSigningKeys.authUpload?.((authDict) => {
- return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
- });
-
- // pass the new keys to the main instance of our own CrossSigningInfo.
- crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys);
- }
- // set account data
- if (this.accountData) {
- for (const [type, content] of this.accountData) {
- await baseApis.setAccountData(type, content);
- }
- }
- // upload first cross-signing signatures with the new key
- // (e.g. signing our own device)
- if (this.keySignatures) {
- await baseApis.uploadKeySignatures(this.keySignatures);
- }
- // need to create/update key backup info
- if (this.keyBackupInfo) {
- if (this.keyBackupInfo.version) {
- // session backup signature
- // The backup is trusted because the user provided the private key.
- // Sign the backup with the cross signing key so the key backup can
- // be trusted via cross-signing.
- await baseApis.http.authedRequest(
- Method.Put,
- "/room_keys/version/" + this.keyBackupInfo.version,
- undefined,
- {
- algorithm: this.keyBackupInfo.algorithm,
- auth_data: this.keyBackupInfo.auth_data,
- },
- { prefix: ClientPrefix.V3 },
- );
- } else {
- // add new key backup
- await baseApis.http.authedRequest(Method.Post, "/room_keys/version", undefined, this.keyBackupInfo, {
- prefix: ClientPrefix.V3,
- });
- }
- }
- }
-}
-
-/**
- * Catches account data set by SecretStorage during bootstrapping by
- * implementing the methods related to account data in MatrixClient
- */
-class AccountDataClientAdapter
- extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
- implements IAccountDataClient
-{
- //
- public readonly values = new Map<string, MatrixEvent>();
-
- /**
- * @param existingValues - existing account data
- */
- public constructor(private readonly existingValues: Map<string, MatrixEvent>) {
- super();
- }
-
- /**
- * @returns the content of the account data
- */
- public getAccountDataFromServer<T extends { [k: string]: any }>(type: string): Promise<T> {
- return Promise.resolve(this.getAccountData(type) as T);
- }
-
- /**
- * @returns the content of the account data
- */
- public getAccountData(type: string): IContent | null {
- const modifiedValue = this.values.get(type);
- if (modifiedValue) {
- return modifiedValue;
- }
- const existingValue = this.existingValues.get(type);
- if (existingValue) {
- return existingValue.getContent();
- }
- return null;
- }
-
- public setAccountData(type: string, content: any): Promise<{}> {
- const lastEvent = this.values.get(type);
- this.values.set(type, content);
- // ensure accountData is emitted on the next tick,
- // as SecretStorage listens for it while calling this method
- // and it seems to rely on this.
- return Promise.resolve().then(() => {
- const event = new MatrixEvent({ type, content });
- this.emit(ClientEvent.AccountData, event, lastEvent);
- return {};
- });
- }
-}
-
-/**
- * Catches the private cross-signing keys set during bootstrapping
- * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
- * See CrossSigningInfo constructor
- */
-class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
- public readonly privateKeys = new Map<string, Uint8Array>();
-
- // cache callbacks
- public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array | null> {
- return this.getCrossSigningKey(type, expectedPublicKey);
- }
-
- public storeCrossSigningKeyCache(type: string, key: Uint8Array): Promise<void> {
- this.privateKeys.set(type, key);
- return Promise.resolve();
- }
-
- // non-cache callbacks
- public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array | null> {
- return Promise.resolve(this.privateKeys.get(type) ?? null);
- }
-
- public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>): void {
- for (const [type, privateKey] of Object.entries(privateKeys)) {
- this.privateKeys.set(type, privateKey);
- }
- }
-}
-
-/**
- * Catches the 4S private key set during bootstrapping by implementing
- * the SecretStorage crypto callbacks
- */
-class SSSSCryptoCallbacks {
- private readonly privateKeys = new Map<string, Uint8Array>();
-
- public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
-
- public async getSecretStorageKey(
- { keys }: { keys: Record<string, SecretStorageKeyDescription> },
- name: string,
- ): Promise<[string, Uint8Array] | null> {
- for (const keyId of Object.keys(keys)) {
- const privateKey = this.privateKeys.get(keyId);
- if (privateKey) {
- return [keyId, privateKey];
- }
- }
- // if we don't have the key cached yet, ask
- // for it to the general crypto callbacks and cache it
- if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
- const result = await this.delegateCryptoCallbacks.getSecretStorageKey({ keys }, name);
- if (result) {
- const [keyId, privateKey] = result;
- this.privateKeys.set(keyId, privateKey);
- }
- return result;
- }
- return null;
- }
-
- public addPrivateKey(keyId: string, keyInfo: SecretStorageKeyDescription, privKey: Uint8Array): void {
- this.privateKeys.set(keyId, privKey);
- // Also pass along to application to cache if it wishes
- this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts
deleted file mode 100644
index 82a0a9a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts
+++ /dev/null
@@ -1,1496 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
-
-import { logger, PrefixedLogger } from "../logger";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import * as algorithms from "./algorithms";
-import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
-import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
-import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto";
-import { IMessage } from "./algorithms/olm";
-
-// The maximum size of an event is 65K, and we base64 the content, so this is a
-// reasonable approximation to the biggest plaintext we can encrypt.
-const MAX_PLAINTEXT_LENGTH = (65536 * 3) / 4;
-
-export class PayloadTooLargeError extends Error {
- public readonly data = {
- errcode: "M_TOO_LARGE",
- error: "Payload too large for encrypted message",
- };
-}
-
-function checkPayloadLength(payloadString: string): void {
- if (payloadString === undefined) {
- throw new Error("payloadString undefined");
- }
-
- if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
- // might as well fail early here rather than letting the olm library throw
- // a cryptic memory allocation error.
- //
- // Note that even if we manage to do the encryption, the message send may fail,
- // because by the time we've wrapped the ciphertext in the event object, it may
- // exceed 65K. But at least we won't just fail with "abort()" in that case.
- throw new PayloadTooLargeError(
- `Message too long (${payloadString.length} bytes). ` +
- `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`,
- );
- }
-}
-
-interface IInitOpts {
- fromExportedDevice?: IExportedDevice;
- pickleKey?: string;
-}
-
-/** data stored in the session store about an inbound group session */
-export interface InboundGroupSessionData {
- room_id: string; // eslint-disable-line camelcase
- /** pickled Olm.InboundGroupSession */
- session: string;
- keysClaimed: Record<string, string>;
- /** Devices involved in forwarding this session to us (normally empty). */
- forwardingCurve25519KeyChain: string[];
- /** whether this session is untrusted. */
- untrusted?: boolean;
- /** whether this session exists during the room being set to shared history. */
- sharedHistory?: boolean;
-}
-
-export interface IDecryptedGroupMessage {
- result: string;
- keysClaimed: Record<string, string>;
- senderKey: string;
- forwardingCurve25519KeyChain: string[];
- untrusted: boolean;
-}
-
-export interface IInboundSession {
- payload: string;
- session_id: string;
-}
-
-export interface IExportedDevice {
- pickleKey: string;
- pickledAccount: string;
- sessions: ISessionInfo[];
-}
-
-interface IUnpickledSessionInfo extends Omit<ISessionInfo, "session"> {
- session: Session;
-}
-
-/* eslint-disable camelcase */
-interface IInboundGroupSessionKey {
- chain_index: number;
- key: string;
- forwarding_curve25519_key_chain: string[];
- sender_claimed_ed25519_key: string | null;
- shared_history: boolean;
- untrusted?: boolean;
-}
-/* eslint-enable camelcase */
-
-type OneTimeKeys = { curve25519: { [keyId: string]: string } };
-
-/**
- * Manages the olm cryptography functions. Each OlmDevice has a single
- * OlmAccount and a number of OlmSessions.
- *
- * Accounts and sessions are kept pickled in the cryptoStore.
- */
-export class OlmDevice {
- public pickleKey = "DEFAULT_KEY"; // set by consumers
-
- /** Curve25519 key for the account, unknown until we load the account from storage in init() */
- public deviceCurve25519Key: string | null = null;
- /** Ed25519 key for the account, unknown until we load the account from storage in init() */
- public deviceEd25519Key: string | null = null;
- private maxOneTimeKeys: number | null = null;
-
- // we don't bother stashing outboundgroupsessions in the cryptoStore -
- // instead we keep them here.
- private outboundGroupSessionStore: Record<string, string> = {};
-
- // Store a set of decrypted message indexes for each group session.
- // This partially mitigates a replay attack where a MITM resends a group
- // message into the room.
- //
- // When we decrypt a message and the message index matches a previously
- // decrypted message, one possible cause of that is that we are decrypting
- // the same event, and may not indicate an actual replay attack. For
- // example, this could happen if we receive events, forget about them, and
- // then re-fetch them when we backfill. So we store the event ID and
- // timestamp corresponding to each message index when we first decrypt it,
- // and compare these against the event ID and timestamp every time we use
- // that same index. If they match, then we're probably decrypting the same
- // event and we don't consider it a replay attack.
- //
- // Keys are strings of form "<senderKey>|<session_id>|<message_index>"
- // Values are objects of the form "{id: <event id>, timestamp: <ts>}"
- private inboundGroupSessionMessageIndexes: Record<string, { id: string; timestamp: number }> = {};
-
- // Keep track of sessions that we're starting, so that we don't start
- // multiple sessions for the same device at the same time.
- public sessionsInProgress: Record<string, Promise<void>> = {}; // set by consumers
-
- // Used by olm to serialise prekey message decryptions
- public olmPrekeyPromise: Promise<any> = Promise.resolve(); // set by consumers
-
- public constructor(private readonly cryptoStore: CryptoStore) {}
-
- /**
- * @returns The version of Olm.
- */
- public static getOlmVersion(): [number, number, number] {
- return global.Olm.get_library_version();
- }
-
- /**
- * Initialise the OlmAccount. This must be called before any other operations
- * on the OlmDevice.
- *
- * Data from an exported Olm device can be provided
- * in order to re-create this device.
- *
- * Attempts to load the OlmAccount from the crypto store, or creates one if none is
- * found.
- *
- * Reads the device keys from the OlmAccount object.
- *
- * @param fromExportedDevice - (Optional) data from exported device
- * that must be re-created.
- * If present, opts.pickleKey is ignored
- * (exported data already provides a pickle key)
- * @param pickleKey - (Optional) pickle key to set instead of default one
- */
- public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise<void> {
- let e2eKeys;
- const account = new global.Olm.Account();
-
- try {
- if (fromExportedDevice) {
- if (pickleKey) {
- logger.warn("ignoring opts.pickleKey" + " because opts.fromExportedDevice is present.");
- }
- this.pickleKey = fromExportedDevice.pickleKey;
- await this.initialiseFromExportedDevice(fromExportedDevice, account);
- } else {
- if (pickleKey) {
- this.pickleKey = pickleKey;
- }
- await this.initialiseAccount(account);
- }
- e2eKeys = JSON.parse(account.identity_keys());
-
- this.maxOneTimeKeys = account.max_number_of_one_time_keys();
- } finally {
- account.free();
- }
-
- this.deviceCurve25519Key = e2eKeys.curve25519;
- this.deviceEd25519Key = e2eKeys.ed25519;
- }
-
- /**
- * Populates the crypto store using data that was exported from an existing device.
- * Note that for now only the “account” and “sessions” stores are populated;
- * Other stores will be as with a new device.
- *
- * @param exportedData - Data exported from another device
- * through the “export” method.
- * @param account - an olm account to initialize
- */
- private async initialiseFromExportedDevice(exportedData: IExportedDevice, account: Account): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.storeAccount(txn, exportedData.pickledAccount);
- exportedData.sessions.forEach((session) => {
- const { deviceKey, sessionId } = session;
- const sessionInfo = {
- session: session.session,
- lastReceivedMessageTs: session.lastReceivedMessageTs,
- };
- this.cryptoStore.storeEndToEndSession(deviceKey!, sessionId!, sessionInfo, txn);
- });
- },
- );
- account.unpickle(this.pickleKey, exportedData.pickledAccount);
- }
-
- private async initialiseAccount(account: Account): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getAccount(txn, (pickledAccount) => {
- if (pickledAccount !== null) {
- account.unpickle(this.pickleKey, pickledAccount);
- } else {
- account.create();
- pickledAccount = account.pickle(this.pickleKey);
- this.cryptoStore.storeAccount(txn, pickledAccount);
- }
- });
- });
- }
-
- /**
- * extract our OlmAccount from the crypto store and call the given function
- * with the account object
- * The `account` object is usable only within the callback passed to this
- * function and will be freed as soon the callback returns. It is *not*
- * usable for the rest of the lifetime of the transaction.
- * This function requires a live transaction object from cryptoStore.doTxn()
- * and therefore may only be called in a doTxn() callback.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private getAccount(txn: unknown, func: (account: Account) => void): void {
- this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
- const account = new global.Olm.Account();
- try {
- account.unpickle(this.pickleKey, pickledAccount!);
- func(account);
- } finally {
- account.free();
- }
- });
- }
-
- /*
- * Saves an account to the crypto store.
- * This function requires a live transaction object from cryptoStore.doTxn()
- * and therefore may only be called in a doTxn() callback.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @param Olm.Account object
- * @internal
- */
- private storeAccount(txn: unknown, account: Account): void {
- this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey));
- }
-
- /**
- * Export data for re-creating the Olm device later.
- * TODO export data other than just account and (P2P) sessions.
- *
- * @returns The exported data
- */
- public async export(): Promise<IExportedDevice> {
- const result: Partial<IExportedDevice> = {
- pickleKey: this.pickleKey,
- };
-
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
- result.pickledAccount = pickledAccount!;
- });
- result.sessions = [];
- // Note that the pickledSession object we get in the callback
- // is not exactly the same thing you get in method _getSession
- // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
- this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
- result.sessions!.push(pickledSession!);
- });
- },
- );
- return result as IExportedDevice;
- }
-
- /**
- * extract an OlmSession from the session store and call the given function
- * The session is usable only within the callback passed to this
- * function and will be freed as soon the callback returns. It is *not*
- * usable for the rest of the lifetime of the transaction.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private getSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
- ): void {
- this.cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => {
- this.unpickleSession(sessionInfo!, func);
- });
- }
-
- /**
- * Creates a session object from a session pickle and executes the given
- * function with it. The session object is destroyed once the function
- * returns.
- *
- * @internal
- */
- private unpickleSession(
- sessionInfo: ISessionInfo,
- func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
- ): void {
- const session = new global.Olm.Session();
- try {
- session.unpickle(this.pickleKey, sessionInfo.session!);
- const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
-
- func(unpickledSessInfo);
- } finally {
- session.free();
- }
- }
-
- /**
- * store our OlmSession in the session store
- *
- * @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}`
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void {
- const sessionId = sessionInfo.session.session_id();
- logger.debug(`Saving Olm session ${sessionId} with device ${deviceKey}: ${sessionInfo.session.describe()}`);
-
- // Why do we re-use the input object for this, overwriting the same key with a different
- // type? Is it because we want to erase the unpickled session to enforce that it's no longer
- // used? A comment would be great.
- const pickledSessionInfo = Object.assign(sessionInfo, {
- session: sessionInfo.session.pickle(this.pickleKey),
- });
- this.cryptoStore.storeEndToEndSession(deviceKey, sessionId, pickledSessionInfo, txn);
- }
-
- /**
- * get an OlmUtility and call the given function
- *
- * @returns result of func
- * @internal
- */
- private getUtility<T>(func: (utility: Utility) => T): T {
- const utility = new global.Olm.Utility();
- try {
- return func(utility);
- } finally {
- utility.free();
- }
- }
-
- /**
- * Signs a message with the ed25519 key for this account.
- *
- * @param message - message to be signed
- * @returns base64-encoded signature
- */
- public async sign(message: string): Promise<string> {
- let result: string;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- result = account.sign(message);
- });
- });
- return result!;
- }
-
- /**
- * Get the current (unused, unpublished) one-time keys for this account.
- *
- * @returns one time keys; an object with the single property
- * <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
- * key.
- */
- public async getOneTimeKeys(): Promise<OneTimeKeys> {
- let result: OneTimeKeys;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- result = JSON.parse(account.one_time_keys());
- });
- });
-
- return result!;
- }
-
- /**
- * Get the maximum number of one-time keys we can store.
- *
- * @returns number of keys
- */
- public maxNumberOfOneTimeKeys(): number {
- return this.maxOneTimeKeys ?? -1;
- }
-
- /**
- * Marks all of the one-time keys as published.
- */
- public async markKeysAsPublished(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- account.mark_keys_as_published();
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate some new one-time keys
- *
- * @param numKeys - number of keys to generate
- * @returns Resolved once the account is saved back having generated the keys
- */
- public generateOneTimeKeys(numKeys: number): Promise<void> {
- return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- account.generate_one_time_keys(numKeys);
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate a new fallback keys
- *
- * @returns Resolved once the account is saved back having generated the key
- */
- public async generateFallbackKey(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- account.generate_fallback_key();
- this.storeAccount(txn, account);
- });
- });
- }
-
- public async getFallbackKey(): Promise<Record<string, Record<string, string>>> {
- let result: Record<string, Record<string, string>>;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- result = JSON.parse(account.unpublished_fallback_key());
- });
- });
- return result!;
- }
-
- public async forgetOldFallbackKey(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- account.forget_old_fallback_key();
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate a new outbound session
- *
- * The new session will be stored in the cryptoStore.
- *
- * @param theirIdentityKey - remote user's Curve25519 identity key
- * @param theirOneTimeKey - remote user's one-time Curve25519 key
- * @returns sessionId for the outbound session.
- */
- public async createOutboundSession(theirIdentityKey: string, theirOneTimeKey: string): Promise<string> {
- let newSessionId: string;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getAccount(txn, (account: Account) => {
- const session = new global.Olm.Session();
- try {
- session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
- newSessionId = session.session_id();
- this.storeAccount(txn, account);
- const sessionInfo: IUnpickledSessionInfo = {
- session,
- // Pretend we've received a message at this point, otherwise
- // if we try to send a message to the device, it won't use
- // this session
- lastReceivedMessageTs: Date.now(),
- };
- this.saveSession(theirIdentityKey, sessionInfo, txn);
- } finally {
- session.free();
- }
- });
- },
- logger.withPrefix("[createOutboundSession]"),
- );
- return newSessionId!;
- }
-
- /**
- * Generate a new inbound session, given an incoming message
- *
- * @param theirDeviceIdentityKey - remote user's Curve25519 identity key
- * @param messageType - messageType field from the received message (must be 0)
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns decrypted payload, and
- * session id of new session
- *
- * @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key).
- */
- public async createInboundSession(
- theirDeviceIdentityKey: string,
- messageType: number,
- ciphertext: string,
- ): Promise<IInboundSession> {
- if (messageType !== 0) {
- throw new Error("Need messageType == 0 to create inbound session");
- }
-
- let result: { payload: string; session_id: string }; // eslint-disable-line camelcase
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getAccount(txn, (account: Account) => {
- const session = new global.Olm.Session();
- try {
- session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
- account.remove_one_time_keys(session);
- this.storeAccount(txn, account);
-
- const payloadString = session.decrypt(messageType, ciphertext);
-
- const sessionInfo: IUnpickledSessionInfo = {
- session,
- // this counts as a received message: set last received message time
- // to now
- lastReceivedMessageTs: Date.now(),
- };
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
-
- result = {
- payload: payloadString,
- session_id: session.session_id(),
- };
- } finally {
- session.free();
- }
- });
- },
- logger.withPrefix("[createInboundSession]"),
- );
-
- return result!;
- }
-
- /**
- * Get a list of known session IDs for the given device
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @returns a list of known session ids for the device
- */
- public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
- const log = logger.withPrefix("[getSessionIdsForDevice]");
-
- if (theirDeviceIdentityKey in this.sessionsInProgress) {
- log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
- try {
- await this.sessionsInProgress[theirDeviceIdentityKey];
- } catch (e) {
- // if the session failed to be created, just fall through and
- // return an empty result
- }
- }
- let sessionIds: string[];
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getEndToEndSessions(theirDeviceIdentityKey, txn, (sessions) => {
- sessionIds = Object.keys(sessions);
- });
- },
- log,
- );
-
- return sessionIds!;
- }
-
- /**
- * Get the right olm session id for encrypting messages to the given identity key
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param nowait - Don't wait for an in-progress session to complete.
- * This should only be set to true of the calling function is the function
- * that marked the session as being in-progress.
- * @param log - A possibly customised log
- * @returns session id, or null if no established session
- */
- public async getSessionIdForDevice(
- theirDeviceIdentityKey: string,
- nowait = false,
- log?: PrefixedLogger,
- ): Promise<string | null> {
- const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
-
- if (sessionInfos.length === 0) {
- return null;
- }
- // Use the session that has most recently received a message
- let idxOfBest = 0;
- for (let i = 1; i < sessionInfos.length; i++) {
- const thisSessInfo = sessionInfos[i];
- const thisLastReceived =
- thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs;
-
- const bestSessInfo = sessionInfos[idxOfBest];
- const bestLastReceived =
- bestSessInfo.lastReceivedMessageTs === undefined ? 0 : bestSessInfo.lastReceivedMessageTs;
- if (
- thisLastReceived > bestLastReceived ||
- (thisLastReceived === bestLastReceived && thisSessInfo.sessionId < bestSessInfo.sessionId)
- ) {
- idxOfBest = i;
- }
- }
- return sessionInfos[idxOfBest].sessionId;
- }
-
- /**
- * Get information on the active Olm sessions for a device.
- * <p>
- * Returns an array, with an entry for each active session. The first entry in
- * the result will be the one used for outgoing messages. Each entry contains
- * the keys 'hasReceivedMessage' (true if the session has received an incoming
- * message and is therefore past the pre-key stage), and 'sessionId'.
- *
- * @param deviceIdentityKey - Curve25519 identity key for the device
- * @param nowait - Don't wait for an in-progress session to complete.
- * This should only be set to true of the calling function is the function
- * that marked the session as being in-progress.
- * @param log - A possibly customised log
- */
- public async getSessionInfoForDevice(
- deviceIdentityKey: string,
- nowait = false,
- log = logger,
- ): Promise<{ sessionId: string; lastReceivedMessageTs: number; hasReceivedMessage: boolean }[]> {
- log = log.withPrefix("[getSessionInfoForDevice]");
-
- if (deviceIdentityKey in this.sessionsInProgress && !nowait) {
- log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
- try {
- await this.sessionsInProgress[deviceIdentityKey];
- } catch (e) {
- // if the session failed to be created, then just fall through and
- // return an empty result
- }
- }
- const info: {
- lastReceivedMessageTs: number;
- hasReceivedMessage: boolean;
- sessionId: string;
- }[] = [];
-
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => {
- const sessionIds = Object.keys(sessions).sort();
- for (const sessionId of sessionIds) {
- this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => {
- info.push({
- lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!,
- hasReceivedMessage: sessInfo.session.has_received_message(),
- sessionId,
- });
- });
- }
- });
- },
- log,
- );
-
- return info;
- }
-
- /**
- * Encrypt an outgoing message using an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param payloadString - payload to be encrypted and sent
- *
- * @returns ciphertext
- */
- public async encryptMessage(
- theirDeviceIdentityKey: string,
- sessionId: string,
- payloadString: string,
- ): Promise<IMessage> {
- checkPayloadLength(payloadString);
-
- let res: IMessage;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
- const sessionDesc = sessionInfo.session.describe();
- logger.log(
- "encryptMessage: Olm Session ID " +
- sessionId +
- " to " +
- theirDeviceIdentityKey +
- ": " +
- sessionDesc,
- );
- res = sessionInfo.session.encrypt(payloadString);
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
- });
- },
- logger.withPrefix("[encryptMessage]"),
- );
- return res!;
- }
-
- /**
- * Decrypt an incoming message using an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param messageType - messageType field from the received message
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns decrypted payload.
- */
- public async decryptMessage(
- theirDeviceIdentityKey: string,
- sessionId: string,
- messageType: number,
- ciphertext: string,
- ): Promise<string> {
- let payloadString: string;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo: IUnpickledSessionInfo) => {
- const sessionDesc = sessionInfo.session.describe();
- logger.log(
- "decryptMessage: Olm Session ID " +
- sessionId +
- " from " +
- theirDeviceIdentityKey +
- ": " +
- sessionDesc,
- );
- payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
- sessionInfo.lastReceivedMessageTs = Date.now();
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
- });
- },
- logger.withPrefix("[decryptMessage]"),
- );
- return payloadString!;
- }
-
- /**
- * Determine if an incoming messages is a prekey message matching an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param messageType - messageType field from the received message
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns true if the received message is a prekey message which matches
- * the given session.
- */
- public async matchesSession(
- theirDeviceIdentityKey: string,
- sessionId: string,
- messageType: number,
- ciphertext: string,
- ): Promise<boolean> {
- if (messageType !== 0) {
- return false;
- }
-
- let matches: boolean;
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
- matches = sessionInfo.session.matches_inbound(ciphertext);
- });
- },
- logger.withPrefix("[matchesSession]"),
- );
- return matches!;
- }
-
- public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`);
- await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
- }
-
- public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
- }
-
- public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- return this.cryptoStore.filterOutNotifiedErrorDevices(devices);
- }
-
- // Outbound group session
- // ======================
-
- /**
- * store an OutboundGroupSession in outboundGroupSessionStore
- *
- * @internal
- */
- private saveOutboundGroupSession(session: OutboundGroupSession): void {
- this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey);
- }
-
- /**
- * extract an OutboundGroupSession from outboundGroupSessionStore and call the
- * given function
- *
- * @returns result of func
- * @internal
- */
- private getOutboundGroupSession<T>(sessionId: string, func: (session: OutboundGroupSession) => T): T {
- const pickled = this.outboundGroupSessionStore[sessionId];
- if (pickled === undefined) {
- throw new Error("Unknown outbound group session " + sessionId);
- }
-
- const session = new global.Olm.OutboundGroupSession();
- try {
- session.unpickle(this.pickleKey, pickled);
- return func(session);
- } finally {
- session.free();
- }
- }
-
- /**
- * Generate a new outbound group session
- *
- * @returns sessionId for the outbound session.
- */
- public createOutboundGroupSession(): string {
- const session = new global.Olm.OutboundGroupSession();
- try {
- session.create();
- this.saveOutboundGroupSession(session);
- return session.session_id();
- } finally {
- session.free();
- }
- }
-
- /**
- * Encrypt an outgoing message with an outbound group session
- *
- * @param sessionId - the id of the outboundgroupsession
- * @param payloadString - payload to be encrypted and sent
- *
- * @returns ciphertext
- */
- public encryptGroupMessage(sessionId: string, payloadString: string): string {
- logger.log(`encrypting msg with megolm session ${sessionId}`);
-
- checkPayloadLength(payloadString);
-
- return this.getOutboundGroupSession(sessionId, (session: OutboundGroupSession) => {
- const res = session.encrypt(payloadString);
- this.saveOutboundGroupSession(session);
- return res;
- });
- }
-
- /**
- * Get the session keys for an outbound group session
- *
- * @param sessionId - the id of the outbound group session
- *
- * @returns current chain index, and
- * base64-encoded secret key.
- */
- public getOutboundGroupSessionKey(sessionId: string): IOutboundGroupSessionKey {
- return this.getOutboundGroupSession(sessionId, function (session: OutboundGroupSession) {
- return {
- chain_index: session.message_index(),
- key: session.session_key(),
- };
- });
- }
-
- // Inbound group session
- // =====================
-
- /**
- * Unpickle a session from a sessionData object and invoke the given function.
- * The session is valid only until func returns.
- *
- * @param sessionData - Object describing the session.
- * @param func - Invoked with the unpickled session
- * @returns result of func
- */
- private unpickleInboundGroupSession<T>(
- sessionData: InboundGroupSessionData,
- func: (session: InboundGroupSession) => T,
- ): T {
- const session = new global.Olm.InboundGroupSession();
- try {
- session.unpickle(this.pickleKey, sessionData.session);
- return func(session);
- } finally {
- session.free();
- }
- }
-
- /**
- * extract an InboundGroupSession from the crypto store and call the given function
- *
- * @param roomId - The room ID to extract the session for, or null to fetch
- * sessions for any room.
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @param func - function to call.
- *
- * @internal
- */
- private getInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn: unknown,
- func: (
- session: InboundGroupSession | null,
- data: InboundGroupSessionData | null,
- withheld: IWithheld | null,
- ) => void,
- ): void {
- this.cryptoStore.getEndToEndInboundGroupSession(
- senderKey,
- sessionId,
- txn,
- (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => {
- if (sessionData === null) {
- func(null, null, withheld);
- return;
- }
-
- // if we were given a room ID, check that the it matches the original one for the session. This stops
- // the HS pretending a message was targeting a different room.
- if (roomId !== null && roomId !== sessionData.room_id) {
- throw new Error(
- "Mismatched room_id for inbound group session (expected " +
- sessionData.room_id +
- ", was " +
- roomId +
- ")",
- );
- }
-
- this.unpickleInboundGroupSession(sessionData, (session: InboundGroupSession) => {
- func(session, sessionData, withheld);
- });
- },
- );
- }
-
- /**
- * Add an inbound group session to the session store
- *
- * @param roomId - room in which this session will be used
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param forwardingCurve25519KeyChain - Devices involved in forwarding
- * this session to us.
- * @param sessionId - session identifier
- * @param sessionKey - base64-encoded secret key
- * @param keysClaimed - Other keys the sender claims.
- * @param exportFormat - true if the megolm keys are in export format
- * (ie, they lack an ed25519 signature)
- * @param extraSessionData - any other data to be include with the session
- */
- public async addInboundGroupSession(
- roomId: string,
- senderKey: string,
- forwardingCurve25519KeyChain: string[],
- sessionId: string,
- sessionKey: string,
- keysClaimed: Record<string, string>,
- exportFormat: boolean,
- extraSessionData: OlmGroupSessionExtraData = {},
- ): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
- ],
- (txn) => {
- /* if we already have this session, consider updating it */
- this.getInboundGroupSession(
- roomId,
- senderKey,
- sessionId,
- txn,
- (
- existingSession: InboundGroupSession | null,
- existingSessionData: InboundGroupSessionData | null,
- ) => {
- // new session.
- const session = new global.Olm.InboundGroupSession();
- try {
- if (exportFormat) {
- session.import_session(sessionKey);
- } else {
- session.create(sessionKey);
- }
- if (sessionId != session.session_id()) {
- throw new Error("Mismatched group session ID from senderKey: " + senderKey);
- }
-
- if (existingSession) {
- logger.log(`Update for megolm session ${senderKey}|${sessionId}`);
- if (existingSession.first_known_index() <= session.first_known_index()) {
- if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
- // existing session has less-than-or-equal index
- // (i.e. can decrypt at least as much), and the
- // new session's trust does not win over the old
- // session's trust, so keep it
- logger.log(`Keeping existing megolm session ${senderKey}|${sessionId}`);
- return;
- }
- if (existingSession.first_known_index() < session.first_known_index()) {
- // We want to upgrade the existing session's trust,
- // but we can't just use the new session because we'll
- // lose the lower index. Check that the sessions connect
- // properly, and then manually set the existing session
- // as trusted.
- if (
- existingSession.export_session(session.first_known_index()) ===
- session.export_session(session.first_known_index())
- ) {
- logger.info(
- "Upgrading trust of existing megolm session " +
- `${senderKey}|${sessionId} based on newly-received trusted session`,
- );
- existingSessionData!.untrusted = false;
- this.cryptoStore.storeEndToEndInboundGroupSession(
- senderKey,
- sessionId,
- existingSessionData!,
- txn,
- );
- } else {
- logger.warn(
- `Newly-received megolm session ${senderKey}|$sessionId}` +
- " does not match existing session! Keeping existing session",
- );
- }
- return;
- }
- // If the sessions have the same index, go ahead and store the new trusted one.
- }
- }
-
- logger.info(
- `Storing megolm session ${senderKey}|${sessionId} with first index ` +
- session.first_known_index(),
- );
-
- const sessionData = Object.assign({}, extraSessionData, {
- room_id: roomId,
- session: session.pickle(this.pickleKey),
- keysClaimed: keysClaimed,
- forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
- });
-
- this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn);
-
- if (!existingSession && extraSessionData.sharedHistory) {
- this.cryptoStore.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
- }
- } finally {
- session.free();
- }
- },
- );
- },
- logger.withPrefix("[addInboundGroupSession]"),
- );
- }
-
- /**
- * Record in the data store why an inbound group session was withheld.
- *
- * @param roomId - room that the session belongs to
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param code - reason code
- * @param reason - human-readable version of `code`
- */
- public async addInboundGroupSessionWithheld(
- roomId: string,
- senderKey: string,
- sessionId: string,
- code: string,
- reason: string,
- ): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
- (txn) => {
- this.cryptoStore.storeEndToEndInboundGroupSessionWithheld(
- senderKey,
- sessionId,
- {
- room_id: roomId,
- code: code,
- reason: reason,
- },
- txn,
- );
- },
- );
- }
-
- /**
- * Decrypt a received message with an inbound group session
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param body - base64-encoded body of the encrypted message
- * @param eventId - ID of the event being decrypted
- * @param timestamp - timestamp of the event being decrypted
- *
- * @returns null if the sessionId is unknown
- */
- public async decryptGroupMessage(
- roomId: string,
- senderKey: string,
- sessionId: string,
- body: string,
- eventId: string,
- timestamp: number,
- ): Promise<IDecryptedGroupMessage | null> {
- let result: IDecryptedGroupMessage | null = null;
- // when the localstorage crypto store is used as an indexeddb backend,
- // exceptions thrown from within the inner function are not passed through
- // to the top level, so we store exceptions in a variable and raise them at
- // the end
- let error: Error;
-
- await this.cryptoStore.doTxn(
- "readwrite",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
- if (session === null || sessionData === null) {
- if (withheld) {
- error = new algorithms.DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- calculateWithheldMessage(withheld),
- {
- session: senderKey + "|" + sessionId,
- },
- );
- }
- result = null;
- return;
- }
- let res: ReturnType<InboundGroupSession["decrypt"]>;
- try {
- res = session.decrypt(body);
- } catch (e) {
- if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
- error = new algorithms.DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- calculateWithheldMessage(withheld),
- {
- session: senderKey + "|" + sessionId,
- },
- );
- } else {
- error = <Error>e;
- }
- return;
- }
-
- let plaintext: string = res.plaintext;
- if (plaintext === undefined) {
- // @ts-ignore - Compatibility for older olm versions.
- plaintext = res as string;
- } else {
- // Check if we have seen this message index before to detect replay attacks.
- // If the event ID and timestamp are specified, and the match the event ID
- // and timestamp from the last time we used this message index, then we
- // don't consider it a replay attack.
- const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
- if (messageIndexKey in this.inboundGroupSessionMessageIndexes) {
- const msgInfo = this.inboundGroupSessionMessageIndexes[messageIndexKey];
- if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) {
- error = new Error(
- "Duplicate message index, possible replay attack: " + messageIndexKey,
- );
- return;
- }
- }
- this.inboundGroupSessionMessageIndexes[messageIndexKey] = {
- id: eventId,
- timestamp: timestamp,
- };
- }
-
- sessionData.session = session.pickle(this.pickleKey);
- this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn);
- result = {
- result: plaintext,
- keysClaimed: sessionData.keysClaimed || {},
- senderKey: senderKey,
- forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
- untrusted: !!sessionData.untrusted,
- };
- });
- },
- logger.withPrefix("[decryptGroupMessage]"),
- );
-
- if (error!) {
- throw error;
- }
- return result!;
- }
-
- /**
- * Determine if we have the keys for a given megolm session
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- *
- * @returns true if we have the keys to this session
- */
- public async hasInboundSessionKeys(roomId: string, senderKey: string, sessionId: string): Promise<boolean> {
- let result: boolean;
- await this.cryptoStore.doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, (sessionData) => {
- if (sessionData === null) {
- result = false;
- return;
- }
-
- if (roomId !== sessionData.room_id) {
- logger.warn(
- `requested keys for inbound group session ${senderKey}|` +
- `${sessionId}, with incorrect room_id ` +
- `(expected ${sessionData.room_id}, ` +
- `was ${roomId})`,
- );
- result = false;
- } else {
- result = true;
- }
- });
- },
- logger.withPrefix("[hasInboundSessionKeys]"),
- );
-
- return result!;
- }
-
- /**
- * Extract the keys to a given megolm session, for sharing
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param chainIndex - The chain index at which to export the session.
- * If omitted, export at the first index we know about.
- *
- * @returns
- * details of the session key. The key is a base64-encoded megolm key in
- * export format.
- *
- * @throws Error If the given chain index could not be obtained from the known
- * index (ie. the given chain index is before the first we have).
- */
- public async getInboundGroupSessionKey(
- roomId: string,
- senderKey: string,
- sessionId: string,
- chainIndex?: number,
- ): Promise<IInboundGroupSessionKey | null> {
- let result: IInboundGroupSessionKey | null = null;
- await this.cryptoStore.doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData) => {
- if (session === null || sessionData === null) {
- result = null;
- return;
- }
-
- if (chainIndex === undefined) {
- chainIndex = session.first_known_index();
- }
-
- const exportedSession = session.export_session(chainIndex);
-
- const claimedKeys = sessionData.keysClaimed || {};
- const senderEd25519Key = claimedKeys.ed25519 || null;
-
- const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
- // older forwarded keys didn't set the "untrusted"
- // property, but can be identified by having a
- // non-empty forwarding key chain. These keys should
- // be marked as untrusted since we don't know that they
- // can be trusted
- const untrusted =
- "untrusted" in sessionData ? sessionData.untrusted : forwardingKeyChain.length > 0;
-
- result = {
- chain_index: chainIndex,
- key: exportedSession,
- forwarding_curve25519_key_chain: forwardingKeyChain,
- sender_claimed_ed25519_key: senderEd25519Key,
- shared_history: sessionData.sharedHistory || false,
- untrusted: untrusted,
- };
- });
- },
- logger.withPrefix("[getInboundGroupSessionKey]"),
- );
-
- return result;
- }
-
- /**
- * Export an inbound group session
- *
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param sessionData - The session object from the store
- * @returns exported session data
- */
- public exportInboundGroupSession(
- senderKey: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- ): IMegolmSessionData {
- return this.unpickleInboundGroupSession(sessionData, (session) => {
- const messageIndex = session.first_known_index();
-
- return {
- "sender_key": senderKey,
- "sender_claimed_keys": sessionData.keysClaimed,
- "room_id": sessionData.room_id,
- "session_id": sessionId,
- "session_key": session.export_session(messageIndex),
- "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [],
- "first_known_index": session.first_known_index(),
- "org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
- } as IMegolmSessionData;
- });
- }
-
- public async getSharedHistoryInboundGroupSessions(
- roomId: string,
- ): Promise<[senderKey: string, sessionId: string][]> {
- let result: Promise<[senderKey: string, sessionId: string][]>;
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS],
- (txn) => {
- result = this.cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
- },
- logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
- );
- return result!;
- }
-
- // Utilities
- // =========
-
- /**
- * Verify an ed25519 signature.
- *
- * @param key - ed25519 key
- * @param message - message which was signed
- * @param signature - base64-encoded signature to be checked
- *
- * @throws Error if there is a problem with the verification. If the key was
- * too small then the message will be "OLM.INVALID_BASE64". If the signature
- * was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
- */
- public verifySignature(key: string, message: string, signature: string): void {
- this.getUtility(function (util: Utility) {
- util.ed25519_verify(key, message, signature);
- });
- }
-}
-
-export const WITHHELD_MESSAGES: Record<string, string> = {
- "m.unverified": "The sender has disabled encrypting to unverified devices.",
- "m.blacklisted": "The sender has blocked you.",
- "m.unauthorised": "You are not authorised to read the message.",
- "m.no_olm": "Unable to establish a secure channel.",
-};
-
-/**
- * Calculate the message to use for the exception when a session key is withheld.
- *
- * @param withheld - An object that describes why the key was withheld.
- *
- * @returns the message
- *
- * @internal
- */
-function calculateWithheldMessage(withheld: IWithheld): string {
- if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
- return WITHHELD_MESSAGES[withheld.code];
- } else if (withheld.reason) {
- return withheld.reason;
- } else {
- return "decryption key withheld";
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts
deleted file mode 100644
index 4628b3e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { v4 as uuidv4 } from "uuid";
-
-import { logger } from "../logger";
-import { MatrixClient } from "../client";
-import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
-import { CryptoStore, OutgoingRoomKeyRequest } from "./store/base";
-import { EventType, ToDeviceMessageId } from "../@types/event";
-import { MapWithDefault } from "../utils";
-
-/**
- * Internal module. Management of outgoing room key requests.
- *
- * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
- * for draft documentation on what we're supposed to be implementing here.
- */
-
-// delay between deciding we want some keys, and sending out the request, to
-// allow for (a) it turning up anyway, (b) grouping requests together
-const SEND_KEY_REQUESTS_DELAY_MS = 500;
-
-/**
- * possible states for a room key request
- *
- * The state machine looks like:
- * ```
- *
- * | (cancellation sent)
- * | .-------------------------------------------------.
- * | | |
- * V V (cancellation requested) |
- * UNSENT -----------------------------+ |
- * | | |
- * | | |
- * | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
- * V | Λ
- * SENT | |
- * |-------------------------------- | --------------'
- * | | (cancellation requested with intent
- * | | to resend the original request)
- * | |
- * | (cancellation requested) |
- * V |
- * CANCELLATION_PENDING |
- * | |
- * | (cancellation sent) |
- * V |
- * (deleted) <---------------------------+
- * ```
- */
-export enum RoomKeyRequestState {
- /** request not yet sent */
- Unsent,
- /** request sent, awaiting reply */
- Sent,
- /** reply received, cancellation not yet sent */
- CancellationPending,
- /**
- * Cancellation not yet sent and will transition to UNSENT instead of
- * being deleted once the cancellation has been sent.
- */
- CancellationPendingAndWillResend,
-}
-
-interface RequestMessageBase {
- requesting_device_id: string;
- request_id: string;
-}
-
-interface RequestMessageRequest extends RequestMessageBase {
- action: "request";
- body: IRoomKeyRequestBody;
-}
-
-interface RequestMessageCancellation extends RequestMessageBase {
- action: "request_cancellation";
-}
-
-type RequestMessage = RequestMessageRequest | RequestMessageCancellation;
-
-export class OutgoingRoomKeyRequestManager {
- // handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
- // if the callback has been set, or if it is still running.
- private sendOutgoingRoomKeyRequestsTimer?: ReturnType<typeof setTimeout>;
-
- // sanity check to ensure that we don't end up with two concurrent runs
- // of sendOutgoingRoomKeyRequests
- private sendOutgoingRoomKeyRequestsRunning = false;
-
- private clientRunning = true;
-
- public constructor(
- private readonly baseApis: MatrixClient,
- private readonly deviceId: string,
- private readonly cryptoStore: CryptoStore,
- ) {}
-
- /**
- * Called when the client is stopped. Stops any running background processes.
- */
- public stop(): void {
- logger.log("stopping OutgoingRoomKeyRequestManager");
- // stop the timer on the next run
- this.clientRunning = false;
- }
-
- /**
- * Send any requests that have been queued
- */
- public sendQueuedRequests(): void {
- this.startTimer();
- }
-
- /**
- * Queue up a room key request, if we haven't already queued or sent one.
- *
- * The `requestBody` is compared (with a deep-equality check) against
- * previous queued or sent requests and if it matches, no change is made.
- * Otherwise, a request is added to the pending list, and a job is started
- * in the background to send it.
- *
- * @param resend - whether to resend the key request if there is
- * already one
- *
- * @returns resolves when the request has been added to the
- * pending list (or we have established that a similar request already
- * exists)
- */
- public async queueRoomKeyRequest(
- requestBody: IRoomKeyRequestBody,
- recipients: IRoomKeyRequestRecipient[],
- resend = false,
- ): Promise<void> {
- const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
- if (!req) {
- await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
- requestBody: requestBody,
- recipients: recipients,
- requestId: this.baseApis.makeTxnId(),
- state: RoomKeyRequestState.Unsent,
- });
- } else {
- switch (req.state) {
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- case RoomKeyRequestState.Unsent:
- // nothing to do here, since we're going to send a request anyways
- return;
-
- case RoomKeyRequestState.CancellationPending: {
- // existing request is about to be cancelled. If we want to
- // resend, then change the state so that it resends after
- // cancelling. Otherwise, just cancel the cancellation.
- const state = resend
- ? RoomKeyRequestState.CancellationPendingAndWillResend
- : RoomKeyRequestState.Sent;
- await this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPending,
- {
- state,
- cancellationTxnId: this.baseApis.makeTxnId(),
- },
- );
- break;
- }
- case RoomKeyRequestState.Sent: {
- // a request has already been sent. If we don't want to
- // resend, then do nothing. If we do want to, then cancel the
- // existing request and send a new one.
- if (resend) {
- const state = RoomKeyRequestState.CancellationPendingAndWillResend;
- const updatedReq = await this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.Sent,
- {
- state,
- cancellationTxnId: this.baseApis.makeTxnId(),
- // need to use a new transaction ID so that
- // the request gets sent
- requestTxnId: this.baseApis.makeTxnId(),
- },
- );
- if (!updatedReq) {
- // updateOutgoingRoomKeyRequest couldn't find the request
- // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
- // raced with another tab to mark the request cancelled.
- // Try again, to make sure the request is resent.
- return this.queueRoomKeyRequest(requestBody, recipients, resend);
- }
-
- // We don't want to wait for the timer, so we send it
- // immediately. (We might actually end up racing with the timer,
- // but that's ok: even if we make the request twice, we'll do it
- // with the same transaction_id, so only one message will get
- // sent).
- //
- // (We also don't want to wait for the response from the server
- // here, as it will slow down processing of received keys if we
- // do.)
- try {
- await this.sendOutgoingRoomKeyRequestCancellation(updatedReq, true);
- } catch (e) {
- logger.error("Error sending room key request cancellation;" + " will retry later.", e);
- }
- // The request has transitioned from
- // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
- // still need to resend the request which is now UNSENT, so
- // start the timer if it isn't already started.
- }
- break;
- }
- default:
- throw new Error("unhandled state: " + req.state);
- }
- }
- }
-
- /**
- * Cancel room key requests, if any match the given requestBody
- *
- *
- * @returns resolves when the request has been updated in our
- * pending list.
- */
- public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<unknown> {
- return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then((req): unknown => {
- if (!req) {
- // no request was made for this key
- return;
- }
- switch (req.state) {
- case RoomKeyRequestState.CancellationPending:
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- // nothing to do here
- return;
-
- case RoomKeyRequestState.Unsent:
- // just delete it
-
- // FIXME: ghahah we may have attempted to send it, and
- // not yet got a successful response. So the server
- // may have seen it, so we still need to send a cancellation
- // in that case :/
-
- logger.log("deleting unnecessary room key request for " + stringifyRequestBody(requestBody));
- return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent);
-
- case RoomKeyRequestState.Sent: {
- // send a cancellation.
- return this.cryptoStore
- .updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, {
- state: RoomKeyRequestState.CancellationPending,
- cancellationTxnId: this.baseApis.makeTxnId(),
- })
- .then((updatedReq) => {
- if (!updatedReq) {
- // updateOutgoingRoomKeyRequest couldn't find the
- // request in state ROOM_KEY_REQUEST_STATES.SENT,
- // so we must have raced with another tab to mark
- // the request cancelled. There is no point in
- // sending another cancellation since the other tab
- // will do it.
- logger.log(
- "Tried to cancel room key request for " +
- stringifyRequestBody(requestBody) +
- " but it was already cancelled in another tab",
- );
- return;
- }
-
- // We don't want to wait for the timer, so we send it
- // immediately. (We might actually end up racing with the timer,
- // but that's ok: even if we make the request twice, we'll do it
- // with the same transaction_id, so only one message will get
- // sent).
- //
- // (We also don't want to wait for the response from the server
- // here, as it will slow down processing of received keys if we
- // do.)
- this.sendOutgoingRoomKeyRequestCancellation(updatedReq).catch((e) => {
- logger.error("Error sending room key request cancellation;" + " will retry later.", e);
- this.startTimer();
- });
- });
- }
- default:
- throw new Error("unhandled state: " + req.state);
- }
- });
- }
-
- /**
- * Look for room key requests by target device and state
- *
- * @param userId - Target user ID
- * @param deviceId - Target device ID
- *
- * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest}
- */
- public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
- return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
- }
-
- /**
- * Find anything in `sent` state, and kick it around the loop again.
- * This is intended for situations where something substantial has changed, and we
- * don't really expect the other end to even care about the cancellation.
- * For example, after initialization or self-verification.
- * @returns An array of `queueRoomKeyRequest` outputs.
- */
- public async cancelAndResendAllOutgoingRequests(): Promise<void[]> {
- const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
- return Promise.all(
- outgoings.map(({ requestBody, recipients }) => this.queueRoomKeyRequest(requestBody, recipients, true)),
- );
- }
-
- // start the background timer to send queued requests, if the timer isn't
- // already running
- private startTimer(): void {
- if (this.sendOutgoingRoomKeyRequestsTimer) {
- return;
- }
-
- const startSendingOutgoingRoomKeyRequests = (): void => {
- if (this.sendOutgoingRoomKeyRequestsRunning) {
- throw new Error("RoomKeyRequestSend already in progress!");
- }
- this.sendOutgoingRoomKeyRequestsRunning = true;
-
- this.sendOutgoingRoomKeyRequests()
- .finally(() => {
- this.sendOutgoingRoomKeyRequestsRunning = false;
- })
- .catch((e) => {
- // this should only happen if there is an indexeddb error,
- // in which case we're a bit stuffed anyway.
- logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`);
- });
- };
-
- this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
- startSendingOutgoingRoomKeyRequests,
- SEND_KEY_REQUESTS_DELAY_MS,
- );
- }
-
- // look for and send any queued requests. Runs itself recursively until
- // there are no more requests, or there is an error (in which case, the
- // timer will be restarted before the promise resolves).
- private async sendOutgoingRoomKeyRequests(): Promise<void> {
- if (!this.clientRunning) {
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- return;
- }
-
- const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([
- RoomKeyRequestState.CancellationPending,
- RoomKeyRequestState.CancellationPendingAndWillResend,
- RoomKeyRequestState.Unsent,
- ]);
-
- if (!req) {
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- return;
- }
-
- try {
- switch (req.state) {
- case RoomKeyRequestState.Unsent:
- await this.sendOutgoingRoomKeyRequest(req);
- break;
- case RoomKeyRequestState.CancellationPending:
- await this.sendOutgoingRoomKeyRequestCancellation(req);
- break;
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- await this.sendOutgoingRoomKeyRequestCancellation(req, true);
- break;
- }
-
- // go around the loop again
- return this.sendOutgoingRoomKeyRequests();
- } catch (e) {
- logger.error("Error sending room key request; will retry later.", e);
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- }
- }
-
- // given a RoomKeyRequest, send it and update the request record
- private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<unknown> {
- logger.log(
- `Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
- ` from ${stringifyRecipientList(req.recipients)}` +
- `(id ${req.requestId})`,
- );
-
- const requestMessage: RequestMessage = {
- action: "request",
- requesting_device_id: this.deviceId,
- request_id: req.requestId,
- body: req.requestBody,
- };
-
- return this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => {
- return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent, {
- state: RoomKeyRequestState.Sent,
- });
- });
- }
-
- // Given a RoomKeyRequest, cancel it and delete the request record unless
- // andResend is set, in which case transition to UNSENT.
- private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<unknown> {
- logger.log(
- `Sending cancellation for key request for ` +
- `${stringifyRequestBody(req.requestBody)} to ` +
- `${stringifyRecipientList(req.recipients)} ` +
- `(cancellation id ${req.cancellationTxnId})`,
- );
-
- const requestMessage: RequestMessage = {
- action: "request_cancellation",
- requesting_device_id: this.deviceId,
- request_id: req.requestId,
- };
-
- return this.sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId).then(() => {
- if (andResend) {
- // We want to resend, so transition to UNSENT
- return this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPendingAndWillResend,
- { state: RoomKeyRequestState.Unsent },
- );
- }
- return this.cryptoStore.deleteOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPending,
- );
- });
- }
-
- // send a RoomKeyRequest to a list of recipients
- private sendMessageToDevices(
- message: RequestMessage,
- recipients: IRoomKeyRequestRecipient[],
- txnId?: string,
- ): Promise<{}> {
- const contentMap = new MapWithDefault<string, Map<string, Record<string, any>>>(() => new Map());
- for (const recip of recipients) {
- const userDeviceMap = contentMap.getOrCreate(recip.userId);
- userDeviceMap.set(recip.deviceId, {
- ...message,
- [ToDeviceMessageId]: uuidv4(),
- });
- }
-
- return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
- }
-}
-
-function stringifyRequestBody(requestBody: IRoomKeyRequestBody): string {
- // we assume that the request is for megolm keys, which are identified by
- // room id and session id
- return requestBody.room_id + " / " + requestBody.session_id;
-}
-
-function stringifyRecipientList(recipients: IRoomKeyRequestRecipient[]): string {
- return `[${recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")}]`;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts
deleted file mode 100644
index a73efcd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Manages the list of encrypted rooms
- */
-
-import { CryptoStore } from "./store/base";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-
-/* eslint-disable camelcase */
-export interface IRoomEncryption {
- algorithm: string;
- rotation_period_ms?: number;
- rotation_period_msgs?: number;
-}
-/* eslint-enable camelcase */
-
-export class RoomList {
- // Object of roomId -> room e2e info object (body of the m.room.encryption event)
- private roomEncryption: Record<string, IRoomEncryption> = {};
-
- public constructor(private readonly cryptoStore?: CryptoStore) {}
-
- public async init(): Promise<void> {
- await this.cryptoStore!.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
- this.cryptoStore!.getEndToEndRooms(txn, (result) => {
- this.roomEncryption = result;
- });
- });
- }
-
- public getRoomEncryption(roomId: string): IRoomEncryption {
- return this.roomEncryption[roomId] || null;
- }
-
- public isRoomEncrypted(roomId: string): boolean {
- return Boolean(this.getRoomEncryption(roomId));
- }
-
- public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise<void> {
- // important that this happens before calling into the store
- // as it prevents the Crypto::setRoomEncryption from calling
- // this twice for consecutive m.room.encryption events
- this.roomEncryption[roomId] = roomInfo;
- await this.cryptoStore!.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
- this.cryptoStore!.storeEndToEndRoom(roomId, roomInfo, txn);
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts
deleted file mode 100644
index 5c9049f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
-Copyright 2019 - 2021 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 { v4 as uuidv4 } from "uuid";
-
-import { logger } from "../logger";
-import * as olmlib from "./olmlib";
-import { randomString } from "../randomstring";
-import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
-import { ICryptoCallbacks, IEncryptedContent } from ".";
-import { IContent, MatrixEvent } from "../models/event";
-import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
-import { IAddSecretStorageKeyOpts } from "./api";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { defer, IDeferred } from "../utils";
-import { ToDeviceMessageId } from "../@types/event";
-import { SecretStorageKeyDescription, SecretStorageKeyDescriptionAesV1 } from "../secret-storage";
-
-export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
-
-// Some of the key functions use a tuple and some use an object...
-export type SecretStorageKeyTuple = [keyId: string, keyInfo: SecretStorageKeyDescription];
-export type SecretStorageKeyObject = { keyId: string; keyInfo: SecretStorageKeyDescription };
-
-export interface ISecretRequest {
- requestId: string;
- promise: Promise<string>;
- cancel: (reason: string) => void;
-}
-
-export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
- // Subset of MatrixClient (which also uses any for the event content)
- getAccountDataFromServer: <T extends { [k: string]: any }>(eventType: string) => Promise<T>;
- getAccountData: (eventType: string) => IContent | null;
- setAccountData: (eventType: string, content: any) => Promise<{}>;
-}
-
-interface ISecretRequestInternal {
- name: string;
- devices: string[];
- deferred: IDeferred<string>;
-}
-
-interface IDecryptors {
- encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
- decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
-}
-
-interface ISecretInfo {
- encrypted: {
- [keyId: string]: IEncryptedPayload;
- };
-}
-
-/**
- * Implements Secure Secret Storage and Sharing (MSC1946)
- */
-export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
- private requests = new Map<string, ISecretRequestInternal>();
-
- // In it's pure javascript days, this was relying on some proper Javascript-style
- // type-abuse where sometimes we'd pass in a fake client object with just the account
- // data methods implemented, which is all this class needs unless you use the secret
- // sharing code, so it was fine. As a low-touch TypeScript migration, this now has
- // an extra, optional param for a real matrix client, so you can not pass it as long
- // as you don't request any secrets.
- // A better solution would probably be to split this class up into secret storage and
- // secret sharing which are really two separate things, even though they share an MSC.
- public constructor(
- private readonly accountDataAdapter: IAccountDataClient,
- private readonly cryptoCallbacks: ICryptoCallbacks,
- private readonly baseApis: B,
- ) {}
-
- public async getDefaultKeyId(): Promise<string | null> {
- const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
- "m.secret_storage.default_key",
- );
- if (!defaultKey) return null;
- return defaultKey.key;
- }
-
- public setDefaultKeyId(keyId: string): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- const listener = (ev: MatrixEvent): void => {
- if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) {
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
- resolve();
- }
- };
- this.accountDataAdapter.on(ClientEvent.AccountData, listener);
-
- this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch((e) => {
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
- reject(e);
- });
- });
- }
-
- /**
- * Add a key for encrypting secrets.
- *
- * @param algorithm - the algorithm used by the key.
- * @param opts - the options for the algorithm. The properties used
- * depend on the algorithm given.
- * @param keyId - the ID of the key. If not given, a random
- * ID will be generated.
- *
- * @returns An object with:
- * keyId: the ID of the key
- * keyInfo: details about the key (iv, mac, passphrase)
- */
- public async addKey(
- algorithm: string,
- opts: IAddSecretStorageKeyOpts = {},
- keyId?: string,
- ): Promise<SecretStorageKeyObject> {
- if (algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
- throw new Error(`Unknown key algorithm ${algorithm}`);
- }
-
- const keyInfo = { algorithm } as SecretStorageKeyDescriptionAesV1;
-
- if (opts.name) {
- keyInfo.name = opts.name;
- }
-
- if (opts.passphrase) {
- keyInfo.passphrase = opts.passphrase;
- }
- if (opts.key) {
- const { iv, mac } = await calculateKeyCheck(opts.key);
- keyInfo.iv = iv;
- keyInfo.mac = mac;
- }
-
- if (!keyId) {
- do {
- keyId = randomString(32);
- } while (
- await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- `m.secret_storage.key.${keyId}`,
- )
- );
- }
-
- await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
-
- return {
- keyId,
- keyInfo,
- };
- }
-
- /**
- * Get the key information for a given ID.
- *
- * @param keyId - The ID of the key to check
- * for. Defaults to the default key ID if not provided.
- * @returns If the key was found, the return value is an array of
- * the form [keyId, keyInfo]. Otherwise, null is returned.
- * XXX: why is this an array when addKey returns an object?
- */
- public async getKey(keyId?: string | null): Promise<SecretStorageKeyTuple | null> {
- if (!keyId) {
- keyId = await this.getDefaultKeyId();
- }
- if (!keyId) {
- return null;
- }
-
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- return keyInfo ? [keyId, keyInfo] : null;
- }
-
- /**
- * Check whether we have a key with a given ID.
- *
- * @param keyId - The ID of the key to check
- * for. Defaults to the default key ID if not provided.
- * @returns Whether we have the key.
- */
- public async hasKey(keyId?: string): Promise<boolean> {
- return Boolean(await this.getKey(keyId));
- }
-
- /**
- * Check whether a key matches what we expect based on the key info
- *
- * @param key - the key to check
- * @param info - the key info
- *
- * @returns whether or not the key matches
- */
- public async checkKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
- if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (info.mac) {
- const { mac } = await calculateKeyCheck(key, info.iv);
- return info.mac.replace(/=+$/g, "") === mac.replace(/=+$/g, "");
- } else {
- // if we have no information, we have to assume the key is right
- return true;
- }
- } else {
- throw new Error("Unknown algorithm");
- }
- }
-
- /**
- * Store an encrypted secret on the server
- *
- * @param name - The name of the secret
- * @param secret - The secret contents.
- * @param keys - The IDs of the keys to use to encrypt the secret
- * or null/undefined to use the default key.
- */
- public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
- const encrypted: Record<string, IEncryptedPayload> = {};
-
- if (!keys) {
- const defaultKeyId = await this.getDefaultKeyId();
- if (!defaultKeyId) {
- throw new Error("No keys specified and no default key present");
- }
- keys = [defaultKeyId];
- }
-
- if (keys.length === 0) {
- throw new Error("Zero keys given to encrypt with!");
- }
-
- for (const keyId of keys) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- if (!keyInfo) {
- throw new Error("Unknown key: " + keyId);
- }
-
- // encrypt secret, based on the algorithm
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- const keys = { [keyId]: keyInfo };
- const [, encryption] = await this.getSecretStorageKey(keys, name);
- encrypted[keyId] = await encryption.encrypt(secret);
- } else {
- logger.warn("unknown algorithm for secret storage key " + keyId + ": " + keyInfo.algorithm);
- // do nothing if we don't understand the encryption algorithm
- }
- }
-
- // save encrypted secret
- await this.accountDataAdapter.setAccountData(name, { encrypted });
- }
-
- /**
- * Get a secret from storage.
- *
- * @param name - the name of the secret
- *
- * @returns the contents of the secret
- */
- public async get(name: string): Promise<string | undefined> {
- const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
- if (!secretInfo) {
- return;
- }
- if (!secretInfo.encrypted) {
- throw new Error("Content is not encrypted!");
- }
-
- // get possible keys to decrypt
- const keys: Record<string, SecretStorageKeyDescription> = {};
- for (const keyId of Object.keys(secretInfo.encrypted)) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- const encInfo = secretInfo.encrypted[keyId];
- // only use keys we understand the encryption algorithm of
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
- keys[keyId] = keyInfo;
- }
- }
- }
-
- if (Object.keys(keys).length === 0) {
- throw new Error(
- `Could not decrypt ${name} because none of ` +
- `the keys it is encrypted with are for a supported algorithm`,
- );
- }
-
- // fetch private key from app
- const [keyId, decryption] = await this.getSecretStorageKey(keys, name);
- const encInfo = secretInfo.encrypted[keyId];
-
- return decryption.decrypt(encInfo);
- }
-
- /**
- * Check if a secret is stored on the server.
- *
- * @param name - the name of the secret
- *
- * @returns map of key name to key info the secret is encrypted
- * with, or null if it is not present or not encrypted with a trusted
- * key
- */
- public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
- // check if secret exists
- const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
- if (!secretInfo?.encrypted) return null;
-
- const ret: Record<string, SecretStorageKeyDescription> = {};
-
- // filter secret encryption keys with supported algorithm
- for (const keyId of Object.keys(secretInfo.encrypted)) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- if (!keyInfo) continue;
- const encInfo = secretInfo.encrypted[keyId];
-
- // only use keys we understand the encryption algorithm of
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
- ret[keyId] = keyInfo;
- }
- }
- }
- return Object.keys(ret).length ? ret : null;
- }
-
- /**
- * Request a secret from another device
- *
- * @param name - the name of the secret to request
- * @param devices - the devices to request the secret from
- */
- public request(this: SecretStorage<MatrixClient>, name: string, devices: string[]): ISecretRequest {
- const requestId = this.baseApis.makeTxnId();
-
- const deferred = defer<string>();
- this.requests.set(requestId, { name, devices, deferred });
-
- const cancel = (reason: string): void => {
- // send cancellation event
- const cancelData = {
- action: "request_cancellation",
- requesting_device_id: this.baseApis.deviceId,
- request_id: requestId,
- };
- const toDevice: Map<string, typeof cancelData> = new Map();
- for (const device of devices) {
- toDevice.set(device, cancelData);
- }
- this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
-
- // and reject the promise so that anyone waiting on it will be
- // notified
- deferred.reject(new Error(reason || "Cancelled"));
- };
-
- // send request to devices
- const requestData = {
- name,
- action: "request",
- requesting_device_id: this.baseApis.deviceId,
- request_id: requestId,
- [ToDeviceMessageId]: uuidv4(),
- };
- const toDevice: Map<string, typeof requestData> = new Map();
- for (const device of devices) {
- toDevice.set(device, requestData);
- }
- logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
- this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
-
- return {
- requestId,
- promise: deferred.promise,
- cancel,
- };
- }
-
- public async onRequestReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): Promise<void> {
- const sender = event.getSender();
- const content = event.getContent();
- if (
- sender !== this.baseApis.getUserId() ||
- !(content.name && content.action && content.requesting_device_id && content.request_id)
- ) {
- // ignore requests from anyone else, for now
- return;
- }
- const deviceId = content.requesting_device_id;
- // check if it's a cancel
- if (content.action === "request_cancellation") {
- /*
- Looks like we intended to emit events when we got cancelations, but
- we never put anything in the _incomingRequests object, and the request
- itself doesn't use events anyway so if we were to wire up cancellations,
- they probably ought to use the same callback interface. I'm leaving them
- disabled for now while converting this file to typescript.
- if (this._incomingRequests[deviceId]
- && this._incomingRequests[deviceId][content.request_id]) {
- logger.info(
- "received request cancellation for secret (" + sender +
- ", " + deviceId + ", " + content.request_id + ")",
- );
- this.baseApis.emit("crypto.secrets.requestCancelled", {
- user_id: sender,
- device_id: deviceId,
- request_id: content.request_id,
- });
- }
- */
- } else if (content.action === "request") {
- if (deviceId === this.baseApis.deviceId) {
- // no point in trying to send ourself the secret
- return;
- }
-
- // check if we have the secret
- logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")");
- if (!this.cryptoCallbacks.onSecretRequested) {
- return;
- }
- const secret = await this.cryptoCallbacks.onSecretRequested(
- sender,
- deviceId,
- content.request_id,
- content.name,
- this.baseApis.checkDeviceTrust(sender, deviceId),
- );
- if (secret) {
- logger.info(`Preparing ${content.name} secret for ${deviceId}`);
- const payload = {
- type: "m.secret.send",
- content: {
- request_id: content.request_id,
- secret: secret,
- },
- };
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.ensureOlmSessionsForDevices(
- this.baseApis.crypto!.olmDevice,
- this.baseApis,
- new Map([[sender, [this.baseApis.getStoredDevice(sender, deviceId)!]]]),
- );
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.baseApis.getUserId()!,
- this.baseApis.deviceId!,
- this.baseApis.crypto!.olmDevice,
- sender,
- this.baseApis.getStoredDevice(sender, deviceId)!,
- payload,
- );
- const contentMap = new Map([[sender, new Map([[deviceId, encryptedContent]])]]);
-
- logger.info(`Sending ${content.name} secret for ${deviceId}`);
- this.baseApis.sendToDevice("m.room.encrypted", contentMap);
- } else {
- logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
- }
- }
- }
-
- public onSecretReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): void {
- if (event.getSender() !== this.baseApis.getUserId()) {
- // we shouldn't be receiving secrets from anyone else, so ignore
- // because someone could be trying to send us bogus data
- return;
- }
-
- if (!olmlib.isOlmEncrypted(event)) {
- logger.error("secret event not properly encrypted");
- return;
- }
-
- const content = event.getContent();
-
- const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
- olmlib.OLM_ALGORITHM,
- event.getSenderKey() || "",
- );
- if (senderKeyUser !== event.getSender()) {
- logger.error("sending device does not belong to the user it claims to be from");
- return;
- }
-
- logger.log("got secret share for request", content.request_id);
- const requestControl = this.requests.get(content.request_id);
- if (requestControl) {
- // make sure that the device that sent it is one of the devices that
- // we requested from
- const deviceInfo = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
- olmlib.OLM_ALGORITHM,
- event.getSenderKey()!,
- );
- if (!deviceInfo) {
- logger.log("secret share from unknown device with key", event.getSenderKey());
- return;
- }
- if (!requestControl.devices.includes(deviceInfo.deviceId)) {
- logger.log("unsolicited secret share from device", deviceInfo.deviceId);
- return;
- }
- // unsure that the sender is trusted. In theory, this check is
- // unnecessary since we only accept secret shares from devices that
- // we requested from, but it doesn't hurt.
- const deviceTrust = this.baseApis.crypto!.checkDeviceInfoTrust(event.getSender()!, deviceInfo);
- if (!deviceTrust.isVerified()) {
- logger.log("secret share from unverified device");
- return;
- }
-
- logger.log(`Successfully received secret ${requestControl.name} ` + `from ${deviceInfo.deviceId}`);
- requestControl.deferred.resolve(content.secret);
- }
- }
-
- private async getSecretStorageKey(
- keys: Record<string, SecretStorageKeyDescription>,
- name: string,
- ): Promise<[string, IDecryptors]> {
- if (!this.cryptoCallbacks.getSecretStorageKey) {
- throw new Error("No getSecretStorageKey callback supplied");
- }
-
- const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
-
- if (!returned) {
- throw new Error("getSecretStorageKey callback returned falsey");
- }
- if (returned.length < 2) {
- throw new Error("getSecretStorageKey callback returned invalid data");
- }
-
- const [keyId, privateKey] = returned;
- if (!keys[keyId]) {
- throw new Error("App returned unknown key from getSecretStorageKey!");
- }
-
- if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- const decryption = {
- encrypt: function (secret: string): Promise<IEncryptedPayload> {
- return encryptAES(secret, privateKey, name);
- },
- decrypt: function (encInfo: IEncryptedPayload): Promise<string> {
- return decryptAES(encInfo, privateKey, name);
- },
- };
- return [keyId, decryption];
- } else {
- throw new Error("Unknown key type: " + keys[keyId].algorithm);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts
deleted file mode 100644
index 48470af..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-Copyright 2020 - 2021 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 "./olmlib";
-import { subtleCrypto, crypto, TextEncoder } from "./crypto";
-
-// 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
- *
- * @param data - the plaintext to encrypt
- * @param key - the encryption key to use
- * @param name - the name of the secret
- * @param ivStr - the initialization vector to use
- */
-export async function encryptAES(
- data: string,
- key: Uint8Array,
- name: string,
- ivStr?: string,
-): Promise<IEncryptedPayload> {
- let iv: Uint8Array;
- if (ivStr) {
- iv = decodeBase64(ivStr);
- } else {
- iv = new Uint8Array(16);
- 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 subtleCrypto.encrypt(
- {
- name: "AES-CTR",
- counter: iv,
- length: 64,
- },
- aesKey,
- encodedData,
- );
-
- const hmac = await subtleCrypto.sign({ name: "HMAC" }, hmacKey, ciphertext);
-
- return {
- iv: encodeBase64(iv),
- ciphertext: encodeBase64(ciphertext),
- mac: encodeBase64(hmac),
- };
-}
-
-/**
- * decrypt a string
- *
- * @param data - the encrypted data
- * @param key - the encryption key to use
- * @param name - the name of the secret
- */
-export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
- const [aesKey, hmacKey] = await deriveKeys(key, name);
-
- const ciphertext = decodeBase64(data.ciphertext);
-
- if (!(await subtleCrypto.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
- throw new Error(`Error decrypting secret ${name}: bad MAC`);
- }
-
- const plaintext = await subtleCrypto.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 subtleCrypto.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
- const keybits = await subtleCrypto.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 = subtleCrypto.importKey("raw", aesKey, { name: "AES-CTR" }, false, ["encrypt", "decrypt"]);
-
- const hmacProm = subtleCrypto.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);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts
deleted file mode 100644
index 6473009..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Internal module. Defines the base classes of the encryption implementations
- */
-
-import type { IMegolmSessionData } from "../../@types/crypto";
-import { MatrixClient } from "../../client";
-import { Room } from "../../models/room";
-import { OlmDevice } from "../OlmDevice";
-import { IContent, MatrixEvent, RoomMember } from "../../matrix";
-import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from "..";
-import { DeviceInfo } from "../deviceinfo";
-import { IRoomEncryption } from "../RoomList";
-import { DeviceInfoMap } from "../DeviceList";
-
-/**
- * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
- */
-export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
-
-export type DecryptionClassParams<P extends IParams = IParams> = Omit<P, "deviceId" | "config">;
-
-/**
- * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class
- */
-export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
-
-export interface IParams {
- /** The UserID for the local user */
- userId: string;
- /** The identifier for this device. */
- deviceId: string;
- /** crypto core */
- crypto: Crypto;
- /** olm.js wrapper */
- olmDevice: OlmDevice;
- /** base matrix api interface */
- baseApis: MatrixClient;
- /** The ID of the room we will be sending to */
- roomId?: string;
- /** The body of the m.room.encryption event */
- config: IRoomEncryption & object;
-}
-
-/**
- * base type for encryption implementations
- */
-export abstract class EncryptionAlgorithm {
- protected readonly userId: string;
- protected readonly deviceId: string;
- protected readonly crypto: Crypto;
- protected readonly olmDevice: OlmDevice;
- protected readonly baseApis: MatrixClient;
- protected readonly roomId?: string;
-
- /**
- * @param params - parameters
- */
- public constructor(params: IParams) {
- this.userId = params.userId;
- this.deviceId = params.deviceId;
- this.crypto = params.crypto;
- this.olmDevice = params.olmDevice;
- this.baseApis = params.baseApis;
- this.roomId = params.roomId;
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- */
- public prepareToEncrypt(room: Room): void {}
-
- /**
- * Encrypt a message event
- *
- * @public
- *
- * @param content - event content
- *
- * @returns Promise which resolves to the new event body
- */
- public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise<IEncryptedContent>;
-
- /**
- * Called when the membership of a member of the room changes.
- *
- * @param event - event causing the change
- * @param member - user whose membership changed
- * @param oldMembership - previous membership
- * @public
- */
- public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
-
- public reshareKeyWithDevice?(
- senderKey: string,
- sessionId: string,
- userId: string,
- device: DeviceInfo,
- ): Promise<void>;
-
- public forceDiscardSession?(): void;
-}
-
-/**
- * base type for decryption implementations
- */
-export abstract class DecryptionAlgorithm {
- protected readonly userId: string;
- protected readonly crypto: Crypto;
- protected readonly olmDevice: OlmDevice;
- protected readonly baseApis: MatrixClient;
- protected readonly roomId?: string;
-
- public constructor(params: DecryptionClassParams) {
- this.userId = params.userId;
- this.crypto = params.crypto;
- this.olmDevice = params.olmDevice;
- this.baseApis = params.baseApis;
- this.roomId = params.roomId;
- }
-
- /**
- * Decrypt an event
- *
- * @param event - undecrypted event
- *
- * @returns promise which
- * resolves once we have finished decrypting. Rejects with an
- * `algorithms.DecryptionError` if there is a problem decrypting the event.
- */
- public abstract decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
-
- /**
- * Handle a key event
- *
- * @param params - event key event
- */
- public async onRoomKeyEvent(params: MatrixEvent): Promise<void> {
- // ignore by default
- }
-
- /**
- * Import a room key
- *
- * @param opts - object
- */
- public async importRoomKey(session: IMegolmSessionData, opts: object): Promise<void> {
- // ignore by default
- }
-
- /**
- * Determine if we have the keys necessary to respond to a room key request
- *
- * @returns true if we have the keys and could (theoretically) share
- * them; else false.
- */
- public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
- return Promise.resolve(false);
- }
-
- /**
- * Send the response to a room key request
- *
- */
- public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
- throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
- }
-
- /**
- * Retry decrypting all the events from a sender that haven't been
- * decrypted yet.
- *
- * @param senderKey - the sender's key
- */
- public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
- // ignore by default
- return false;
- }
-
- public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
- public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
-}
-
-/**
- * Exception thrown when decryption fails
- *
- * @param msg - user-visible message describing the problem
- *
- * @param details - key/value pairs reported in the logs but not shown
- * to the user.
- */
-export class DecryptionError extends Error {
- public readonly detailedString: string;
-
- public constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
- super(msg);
- this.code = code;
- this.name = "DecryptionError";
- this.detailedString = detailedStringForDecryptionError(this, details);
- }
-}
-
-function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
- let result = err.name + "[msg: " + err.message;
-
- if (details) {
- result +=
- ", " +
- Object.keys(details)
- .map((k) => k + ": " + details[k])
- .join(", ");
- }
-
- result += "]";
-
- return result;
-}
-
-export class UnknownDeviceError extends Error {
- /**
- * Exception thrown specifically when we want to warn the user to consider
- * the security of their conversation before continuing
- *
- * @param msg - message describing the problem
- * @param devices - set of unknown devices per user we're warning about
- */
- public constructor(msg: string, public readonly devices: DeviceInfoMap, public event?: MatrixEvent) {
- super(msg);
- this.name = "UnknownDeviceError";
- this.devices = devices;
- }
-}
-
-/**
- * Registers an encryption/decryption class for a particular algorithm
- *
- * @param algorithm - algorithm tag to register for
- *
- * @param encryptor - {@link EncryptionAlgorithm} implementation
- *
- * @param decryptor - {@link DecryptionAlgorithm} implementation
- */
-export function registerAlgorithm<P extends IParams = IParams>(
- algorithm: string,
- encryptor: new (params: P) => EncryptionAlgorithm,
- decryptor: new (params: DecryptionClassParams<P>) => DecryptionAlgorithm,
-): void {
- ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm);
- DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts
deleted file mode 100644
index b3c5b0e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import "./olm";
-import "./megolm";
-
-export * from "./base";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts
deleted file mode 100644
index 061e169..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts
+++ /dev/null
@@ -1,2208 +0,0 @@
-/*
-Copyright 2015 - 2021, 2023 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/**
- * Defines m.olm encryption/decryption
- */
-
-import { v4 as uuidv4 } from "uuid";
-
-import type { IEventDecryptionResult, IMegolmSessionData } from "../../@types/crypto";
-import { logger, PrefixedLogger } from "../../logger";
-import * as olmlib from "../olmlib";
-import {
- DecryptionAlgorithm,
- DecryptionClassParams,
- DecryptionError,
- EncryptionAlgorithm,
- IParams,
- registerAlgorithm,
- UnknownDeviceError,
-} from "./base";
-import { IDecryptedGroupMessage, WITHHELD_MESSAGES } from "../OlmDevice";
-import { Room } from "../../models/room";
-import { DeviceInfo } from "../deviceinfo";
-import { IOlmSessionResult } from "../olmlib";
-import { DeviceInfoMap } from "../DeviceList";
-import { IContent, MatrixEvent } from "../../models/event";
-import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event";
-import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } from "../index";
-import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
-import { OlmGroupSessionExtraData } from "../../@types/crypto";
-import { MatrixError } from "../../http-api";
-import { immediate, MapWithDefault } from "../../utils";
-
-// determine whether the key can be shared with invitees
-export function isRoomSharedHistory(room: Room): boolean {
- const visibilityEvent = room?.currentState?.getStateEvents("m.room.history_visibility", "");
- // NOTE: if the room visibility is unset, it would normally default to
- // "world_readable".
- // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
- // But we will be paranoid here, and treat it as a situation where the room
- // is not shared-history
- const visibility = visibilityEvent?.getContent()?.history_visibility;
- return ["world_readable", "shared"].includes(visibility);
-}
-
-interface IBlockedDevice {
- code: string;
- reason: string;
- deviceInfo: DeviceInfo;
-}
-
-// map user Id → device Id → IBlockedDevice
-type BlockedMap = Map<string, Map<string, IBlockedDevice>>;
-
-export interface IOlmDevice<T = DeviceInfo> {
- userId: string;
- deviceInfo: T;
-}
-
-/**
- * Tests whether an encrypted content has a ciphertext.
- * Ciphertext can be a string or object depending on the content type {@link IEncryptedContent}.
- *
- * @param content - Encrypted content
- * @returns true: has ciphertext, else false
- */
-const hasCiphertext = (content: IEncryptedContent): boolean => {
- return typeof content.ciphertext === "string"
- ? !!content.ciphertext.length
- : !!Object.keys(content.ciphertext).length;
-};
-
-/** The result of parsing the an `m.room_key` or `m.forwarded_room_key` to-device event */
-interface RoomKey {
- /**
- * The Curve25519 key of the megolm session creator.
- *
- * For `m.room_key`, this is also the sender of the `m.room_key` to-device event.
- * For `m.forwarded_room_key`, the two are different (and the key of the sender of the
- * `m.forwarded_room_key` event is included in `forwardingKeyChain`)
- */
- senderKey: string;
- sessionId: string;
- sessionKey: string;
- exportFormat: boolean;
- roomId: string;
- algorithm: string;
- /**
- * A list of the curve25519 keys of the users involved in forwarding this key, most recent last.
- * For `m.room_key` events, this is empty.
- */
- forwardingKeyChain: string[];
- keysClaimed: Partial<Record<"ed25519", string>>;
- extraSessionData: OlmGroupSessionExtraData;
-}
-
-export interface IOutboundGroupSessionKey {
- chain_index: number;
- key: string;
-}
-
-interface IMessage {
- type: string;
- content: {
- "algorithm": string;
- "room_id": string;
- "sender_key"?: string;
- "sender_claimed_ed25519_key"?: string;
- "session_id": string;
- "session_key": string;
- "chain_index": number;
- "forwarding_curve25519_key_chain"?: string[];
- "org.matrix.msc3061.shared_history": boolean;
- };
-}
-
-interface IKeyForwardingMessage extends IMessage {
- type: "m.forwarded_room_key";
-}
-
-interface IPayload extends Partial<IMessage> {
- code?: string;
- reason?: string;
- room_id?: string;
- session_id?: string;
- algorithm?: string;
- sender_key?: string;
-}
-
-interface SharedWithData {
- // The identity key of the device we shared with
- deviceKey: string;
- // The message index of the ratchet we shared with that device
- messageIndex: number;
-}
-
-/**
- * @internal
- */
-class OutboundSessionInfo {
- /** number of times this session has been used */
- public useCount = 0;
- /** when the session was created (ms since the epoch) */
- public creationTime: number;
- /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */
- public sharedWithDevices: MapWithDefault<string, Map<string, SharedWithData>> = new MapWithDefault(() => new Map());
- public blockedDevicesNotified: MapWithDefault<string, Map<string, boolean>> = new MapWithDefault(() => new Map());
-
- /**
- * @param sharedHistory - whether the session can be freely shared with
- * other group members, according to the room history visibility settings
- */
- public constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
- this.creationTime = new Date().getTime();
- }
-
- /**
- * Check if it's time to rotate the session
- */
- public needsRotation(rotationPeriodMsgs: number, rotationPeriodMs: number): boolean {
- const sessionLifetime = new Date().getTime() - this.creationTime;
-
- if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
- logger.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms");
- return true;
- }
-
- return false;
- }
-
- public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
- this.sharedWithDevices.getOrCreate(userId).set(deviceId, { deviceKey, messageIndex: chainIndex });
- }
-
- public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
- this.blockedDevicesNotified.getOrCreate(userId).set(deviceId, true);
- }
-
- /**
- * Determine if this session has been shared with devices which it shouldn't
- * have been.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- *
- * @returns true if we have shared the session with devices which aren't
- * in devicesInRoom.
- */
- public sharedWithTooManyDevices(devicesInRoom: DeviceInfoMap): boolean {
- for (const [userId, devices] of this.sharedWithDevices) {
- if (!devicesInRoom.has(userId)) {
- logger.log("Starting new megolm session because we shared with " + userId);
- return true;
- }
-
- for (const [deviceId] of devices) {
- if (!devicesInRoom.get(userId)?.get(deviceId)) {
- logger.log("Starting new megolm session because we shared with " + userId + ":" + deviceId);
- return true;
- }
- }
- }
-
- return false;
- }
-}
-
-/**
- * Megolm encryption implementation
- *
- * @param params - parameters, as per {@link EncryptionAlgorithm}
- */
-export class MegolmEncryption extends EncryptionAlgorithm {
- // the most recent attempt to set up a session. This is used to serialise
- // the session setups, so that we have a race-free view of which session we
- // are using, and which devices we have shared the keys with. It resolves
- // with an OutboundSessionInfo (or undefined, for the first message in the
- // room).
- private setupPromise = Promise.resolve<OutboundSessionInfo | null>(null);
-
- // Map of outbound sessions by sessions ID. Used if we need a particular
- // session (the session we're currently using to send is always obtained
- // using setupPromise).
- private outboundSessions: Record<string, OutboundSessionInfo> = {};
-
- private readonly sessionRotationPeriodMsgs: number;
- private readonly sessionRotationPeriodMs: number;
- private encryptionPreparation?: {
- promise: Promise<void>;
- startTime: number;
- cancel: () => void;
- };
-
- protected readonly roomId: string;
- private readonly prefixedLogger: PrefixedLogger;
-
- public constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
- super(params);
- this.roomId = params.roomId;
- this.prefixedLogger = logger.withPrefix(`[${this.roomId} encryption]`);
-
- this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
- this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000;
- }
-
- /**
- * @internal
- *
- * @param devicesInRoom - The devices in this room, indexed by user ID
- * @param blocked - The devices that are blocked, indexed by user ID
- * @param singleOlmCreationPhase - Only perform one round of olm
- * session creation
- *
- * This method updates the setupPromise field of the class by chaining a new
- * call on top of the existing promise, and then catching and discarding any
- * errors that might happen while setting up the outbound group session. This
- * is done to ensure that `setupPromise` always resolves to `null` or the
- * `OutboundSessionInfo`.
- *
- * Using `>>=` to represent the promise chaining operation, it does the
- * following:
- *
- * ```
- * setupPromise = previousSetupPromise >>= setup >>= discardErrors
- * ```
- *
- * The initial value for the `setupPromise` is a promise that resolves to
- * `null`. The forceDiscardSession() resets setupPromise to this initial
- * promise.
- *
- * @returns Promise which resolves to the
- * OutboundSessionInfo when setup is complete.
- */
- private async ensureOutboundSession(
- room: Room,
- devicesInRoom: DeviceInfoMap,
- blocked: BlockedMap,
- singleOlmCreationPhase = false,
- ): Promise<OutboundSessionInfo> {
- // takes the previous OutboundSessionInfo, and considers whether to create
- // a new one. Also shares the key with any (new) devices in the room.
- //
- // returns a promise which resolves once the keyshare is successful.
- const setup = async (oldSession: OutboundSessionInfo | null): Promise<OutboundSessionInfo> => {
- const sharedHistory = isRoomSharedHistory(room);
- const session = await this.prepareSession(devicesInRoom, sharedHistory, oldSession);
-
- await this.shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session);
-
- return session;
- };
-
- // first wait for the previous share to complete
- const fallible = this.setupPromise.then(setup);
-
- // Ensure any failures are logged for debugging and make sure that the
- // promise chain remains unbroken
- //
- // setupPromise resolves to `null` or the `OutboundSessionInfo` whether
- // or not the share succeeds
- this.setupPromise = fallible.catch((e) => {
- this.prefixedLogger.error(`Failed to setup outbound session`, e);
- return null;
- });
-
- // but we return a promise which only resolves if the share was successful.
- return fallible;
- }
-
- private async prepareSession(
- devicesInRoom: DeviceInfoMap,
- sharedHistory: boolean,
- session: OutboundSessionInfo | null,
- ): Promise<OutboundSessionInfo> {
- // history visibility changed
- if (session && sharedHistory !== session.sharedHistory) {
- session = null;
- }
-
- // need to make a brand new session?
- if (session?.needsRotation(this.sessionRotationPeriodMsgs, this.sessionRotationPeriodMs)) {
- this.prefixedLogger.log("Starting new megolm session because we need to rotate.");
- session = null;
- }
-
- // determine if we have shared with anyone we shouldn't have
- if (session?.sharedWithTooManyDevices(devicesInRoom)) {
- session = null;
- }
-
- if (!session) {
- this.prefixedLogger.log("Starting new megolm session");
- session = await this.prepareNewSession(sharedHistory);
- this.prefixedLogger.log(`Started new megolm session ${session.sessionId}`);
- this.outboundSessions[session.sessionId] = session;
- }
-
- return session;
- }
-
- private async shareSession(
- devicesInRoom: DeviceInfoMap,
- sharedHistory: boolean,
- singleOlmCreationPhase: boolean,
- blocked: BlockedMap,
- session: OutboundSessionInfo,
- ): Promise<void> {
- // now check if we need to share with any devices
- const shareMap: Record<string, DeviceInfo[]> = {};
-
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, deviceInfo] of userDevices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother sending to ourself
- continue;
- }
-
- if (!session.sharedWithDevices.get(userId)?.get(deviceId)) {
- shareMap[userId] = shareMap[userId] || [];
- shareMap[userId].push(deviceInfo);
- }
- }
- }
-
- const key = this.olmDevice.getOutboundGroupSessionKey(session.sessionId);
- const payload: IPayload = {
- type: "m.room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": this.roomId,
- "session_id": session.sessionId,
- "session_key": key.key,
- "chain_index": key.chain_index,
- "org.matrix.msc3061.shared_history": sharedHistory,
- },
- };
- const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
- this.olmDevice,
- this.baseApis,
- shareMap,
- );
-
- await Promise.all([
- (async (): Promise<void> => {
- // share keys with devices that we already have a session for
- const olmSessionList = Array.from(olmSessions.entries())
- .map(([userId, sessionsByUser]) =>
- Array.from(sessionsByUser.entries()).map(
- ([deviceId, session]) => `${userId}/${deviceId}: ${session.sessionId}`,
- ),
- )
- .flat(1);
- this.prefixedLogger.debug("Sharing keys with devices with existing Olm sessions:", olmSessionList);
- await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
- this.prefixedLogger.debug("Shared keys with existing Olm sessions");
- })(),
- (async (): Promise<void> => {
- const deviceList = Array.from(devicesWithoutSession.entries())
- .map(([userId, devicesByUser]) => devicesByUser.map((device) => `${userId}/${device.deviceId}`))
- .flat(1);
- this.prefixedLogger.debug(
- "Sharing keys (start phase 1) with devices without existing Olm sessions:",
- deviceList,
- );
- const errorDevices: IOlmDevice[] = [];
-
- // meanwhile, establish olm sessions for devices that we don't
- // already have a session for, and share keys with them. If
- // we're doing two phases of olm session creation, use a
- // shorter timeout when fetching one-time keys for the first
- // phase.
- const start = Date.now();
- const failedServers: string[] = [];
- await this.shareKeyWithDevices(
- session,
- key,
- payload,
- devicesWithoutSession,
- errorDevices,
- singleOlmCreationPhase ? 10000 : 2000,
- failedServers,
- );
- this.prefixedLogger.debug("Shared keys (end phase 1) with devices without existing Olm sessions");
-
- if (!singleOlmCreationPhase && Date.now() - start < 10000) {
- // perform the second phase of olm session creation if requested,
- // and if the first phase didn't take too long
- (async (): Promise<void> => {
- // Retry sending keys to devices that we were unable to establish
- // an olm session for. This time, we use a longer timeout, but we
- // do this in the background and don't block anything else while we
- // do this. We only need to retry users from servers that didn't
- // respond the first time.
- const retryDevices: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
- const failedServerMap = new Set();
- for (const server of failedServers) {
- failedServerMap.add(server);
- }
- const failedDevices: IOlmDevice[] = [];
- for (const { userId, deviceInfo } of errorDevices) {
- const userHS = userId.slice(userId.indexOf(":") + 1);
- if (failedServerMap.has(userHS)) {
- retryDevices.getOrCreate(userId).push(deviceInfo);
- } else {
- // if we aren't going to retry, then handle it
- // as a failed device
- failedDevices.push({ userId, deviceInfo });
- }
- }
-
- const retryDeviceList = Array.from(retryDevices.entries())
- .map(([userId, devicesByUser]) =>
- devicesByUser.map((device) => `${userId}/${device.deviceId}`),
- )
- .flat(1);
-
- if (retryDeviceList.length > 0) {
- this.prefixedLogger.debug(
- "Sharing keys (start phase 2) with devices without existing Olm sessions:",
- retryDeviceList,
- );
- await this.shareKeyWithDevices(session, key, payload, retryDevices, failedDevices, 30000);
- this.prefixedLogger.debug(
- "Shared keys (end phase 2) with devices without existing Olm sessions",
- );
- }
-
- await this.notifyFailedOlmDevices(session, key, failedDevices);
- })();
- } else {
- await this.notifyFailedOlmDevices(session, key, errorDevices);
- }
- })(),
- (async (): Promise<void> => {
- this.prefixedLogger.debug(
- `There are ${blocked.size} blocked devices:`,
- Array.from(blocked.entries())
- .map(([userId, blockedByUser]) =>
- Array.from(blockedByUser.entries()).map(
- ([deviceId, _deviceInfo]) => `${userId}/${deviceId}`,
- ),
- )
- .flat(1),
- );
-
- // also, notify newly blocked devices that they're blocked
- const blockedMap: MapWithDefault<string, Map<string, { device: IBlockedDevice }>> = new MapWithDefault(
- () => new Map(),
- );
- let blockedCount = 0;
- for (const [userId, userBlockedDevices] of blocked) {
- for (const [deviceId, device] of userBlockedDevices) {
- if (session.blockedDevicesNotified.get(userId)?.get(deviceId) === undefined) {
- blockedMap.getOrCreate(userId).set(deviceId, { device });
- blockedCount++;
- }
- }
- }
-
- if (blockedCount) {
- this.prefixedLogger.debug(
- `Notifying ${blockedCount} newly blocked devices:`,
- Array.from(blockedMap.entries())
- .map(([userId, blockedByUser]) =>
- Object.entries(blockedByUser).map(([deviceId, _deviceInfo]) => `${userId}/${deviceId}`),
- )
- .flat(1),
- );
- await this.notifyBlockedDevices(session, blockedMap);
- this.prefixedLogger.debug(`Notified ${blockedCount} newly blocked devices`);
- }
- })(),
- ]);
- }
-
- /**
- * @internal
- *
- *
- * @returns session
- */
- private async prepareNewSession(sharedHistory: boolean): Promise<OutboundSessionInfo> {
- const sessionId = this.olmDevice.createOutboundGroupSession();
- const key = this.olmDevice.getOutboundGroupSessionKey(sessionId);
-
- await this.olmDevice.addInboundGroupSession(
- this.roomId,
- this.olmDevice.deviceCurve25519Key!,
- [],
- sessionId,
- key.key,
- { ed25519: this.olmDevice.deviceEd25519Key! },
- false,
- { sharedHistory },
- );
-
- // don't wait for it to complete
- this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId);
-
- return new OutboundSessionInfo(sessionId, sharedHistory);
- }
-
- /**
- * Determines what devices in devicesByUser don't have an olm session as given
- * in devicemap.
- *
- * @internal
- *
- * @param deviceMap - the devices that have olm sessions, as returned by
- * olmlib.ensureOlmSessionsForDevices.
- * @param devicesByUser - a map of user IDs to array of deviceInfo
- * @param noOlmDevices - an array to fill with devices that don't have
- * olm sessions
- *
- * @returns an array of devices that don't have olm sessions. If
- * noOlmDevices is specified, then noOlmDevices will be returned.
- */
- private getDevicesWithoutSessions(
- deviceMap: Map<string, Map<string, IOlmSessionResult>>,
- devicesByUser: Map<string, DeviceInfo[]>,
- noOlmDevices: IOlmDevice[] = [],
- ): IOlmDevice[] {
- for (const [userId, devicesToShareWith] of devicesByUser) {
- const sessionResults = deviceMap.get(userId);
-
- for (const deviceInfo of devicesToShareWith) {
- const deviceId = deviceInfo.deviceId;
-
- const sessionResult = sessionResults?.get(deviceId);
- if (!sessionResult?.sessionId) {
- // no session with this device, probably because there
- // were no one-time keys.
-
- noOlmDevices.push({ userId, deviceInfo });
- sessionResults?.delete(deviceId);
-
- // ensureOlmSessionsForUsers has already done the logging,
- // so just skip it.
- continue;
- }
- }
- }
-
- return noOlmDevices;
- }
-
- /**
- * Splits the user device map into multiple chunks to reduce the number of
- * devices we encrypt to per API call.
- *
- * @internal
- *
- * @param devicesByUser - map from userid to list of devices
- *
- * @returns the blocked devices, split into chunks
- */
- private splitDevices<T extends DeviceInfo | IBlockedDevice>(
- devicesByUser: Map<string, Map<string, { device: T }>>,
- ): IOlmDevice<T>[][] {
- const maxDevicesPerRequest = 20;
-
- // use an array where the slices of a content map gets stored
- let currentSlice: IOlmDevice<T>[] = [];
- const mapSlices = [currentSlice];
-
- for (const [userId, userDevices] of devicesByUser) {
- for (const deviceInfo of userDevices.values()) {
- currentSlice.push({
- userId: userId,
- deviceInfo: deviceInfo.device,
- });
- }
-
- // We do this in the per-user loop as we prefer that all messages to the
- // same user end up in the same API call to make it easier for the
- // server (e.g. only have to send one EDU if a remote user, etc). This
- // does mean that if a user has many devices we may go over the desired
- // limit, but its not a hard limit so that is fine.
- if (currentSlice.length > maxDevicesPerRequest) {
- // the current slice is filled up. Start inserting into the next slice
- currentSlice = [];
- mapSlices.push(currentSlice);
- }
- }
- if (currentSlice.length === 0) {
- mapSlices.pop();
- }
- return mapSlices;
- }
-
- /**
- * @internal
- *
- *
- * @param chainIndex - current chain index
- *
- * @param userDeviceMap - mapping from userId to deviceInfo
- *
- * @param payload - fields to include in the encrypted payload
- *
- * @returns Promise which resolves once the key sharing
- * for the given userDeviceMap is generated and has been sent.
- */
- private encryptAndSendKeysToDevices(
- session: OutboundSessionInfo,
- chainIndex: number,
- devices: IOlmDevice[],
- payload: IPayload,
- ): Promise<void> {
- return this.crypto
- .encryptAndSendToDevices(devices, payload)
- .then(() => {
- // store that we successfully uploaded the keys of the current slice
- for (const device of devices) {
- session.markSharedWithDevice(
- device.userId,
- device.deviceInfo.deviceId,
- device.deviceInfo.getIdentityKey(),
- chainIndex,
- );
- }
- })
- .catch((error) => {
- this.prefixedLogger.error("failed to encryptAndSendToDevices", error);
- throw error;
- });
- }
-
- /**
- * @internal
- *
- *
- * @param userDeviceMap - list of blocked devices to notify
- *
- * @param payload - fields to include in the notification payload
- *
- * @returns Promise which resolves once the notifications
- * for the given userDeviceMap is generated and has been sent.
- */
- private async sendBlockedNotificationsToDevices(
- session: OutboundSessionInfo,
- userDeviceMap: IOlmDevice<IBlockedDevice>[],
- payload: IPayload,
- ): Promise<void> {
- const contentMap: MapWithDefault<string, Map<string, IPayload>> = new MapWithDefault(() => new Map());
-
- for (const val of userDeviceMap) {
- const userId = val.userId;
- const blockedInfo = val.deviceInfo;
- const deviceInfo = blockedInfo.deviceInfo;
- const deviceId = deviceInfo.deviceId;
-
- const message = {
- ...payload,
- code: blockedInfo.code,
- reason: blockedInfo.reason,
- [ToDeviceMessageId]: uuidv4(),
- };
-
- if (message.code === "m.no_olm") {
- delete message.room_id;
- delete message.session_id;
- }
-
- contentMap.getOrCreate(userId).set(deviceId, message);
- }
-
- await this.baseApis.sendToDevice("m.room_key.withheld", contentMap);
-
- // record the fact that we notified these blocked devices
- for (const [userId, userDeviceMap] of contentMap) {
- for (const deviceId of userDeviceMap.keys()) {
- session.markNotifiedBlockedDevice(userId, deviceId);
- }
- }
- }
-
- /**
- * Re-shares a megolm session key with devices if the key has already been
- * sent to them.
- *
- * @param senderKey - The key of the originating device for the session
- * @param sessionId - ID of the outbound session to share
- * @param userId - ID of the user who owns the target device
- * @param device - The target device
- */
- public async reshareKeyWithDevice(
- senderKey: string,
- sessionId: string,
- userId: string,
- device: DeviceInfo,
- ): Promise<void> {
- const obSessionInfo = this.outboundSessions[sessionId];
- if (!obSessionInfo) {
- this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} not found: not re-sharing keys`);
- return;
- }
-
- // The chain index of the key we previously sent this device
- if (!obSessionInfo.sharedWithDevices.has(userId)) {
- this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} never shared with user ${userId}`);
- return;
- }
- const sessionSharedData = obSessionInfo.sharedWithDevices.get(userId)?.get(device.deviceId);
- if (sessionSharedData === undefined) {
- this.prefixedLogger.debug(
- `megolm session ${senderKey}|${sessionId} never shared with device ${userId}:${device.deviceId}`,
- );
- return;
- }
-
- if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
- this.prefixedLogger.warn(
- `Megolm session ${senderKey}|${sessionId} has been shared with device ${device.deviceId} but ` +
- `with identity key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
- );
- return;
- }
-
- // get the key from the inbound session: the outbound one will already
- // have been ratcheted to the next chain index.
- const key = await this.olmDevice.getInboundGroupSessionKey(
- this.roomId,
- senderKey,
- sessionId,
- sessionSharedData.messageIndex,
- );
-
- if (!key) {
- this.prefixedLogger.warn(
- `No inbound session key found for megolm session ${senderKey}|${sessionId}: not re-sharing keys`,
- );
- return;
- }
-
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [device]]]));
-
- const payload = {
- type: "m.forwarded_room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": this.roomId,
- "session_id": sessionId,
- "session_key": key.key,
- "chain_index": key.chain_index,
- "sender_key": senderKey,
- "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
- "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
- "org.matrix.msc3061.shared_history": key.shared_history || false,
- },
- };
-
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- device,
- payload,
- );
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[userId, new Map([[device.deviceId, encryptedContent]])]]),
- );
- this.prefixedLogger.debug(
- `Re-shared key for megolm session ${senderKey}|${sessionId} with ${userId}:${device.deviceId}`,
- );
- }
-
- /**
- * @internal
- *
- *
- * @param key - the session key as returned by
- * OlmDevice.getOutboundGroupSessionKey
- *
- * @param payload - the base to-device message payload for sharing keys
- *
- * @param devicesByUser - map from userid to list of devices
- *
- * @param errorDevices - array that will be populated with the devices that we can't get an
- * olm session for
- *
- * @param otkTimeout - The timeout in milliseconds when requesting
- * one-time keys for establishing new olm sessions.
- *
- * @param failedServers - An array to fill with remote servers that
- * failed to respond to one-time-key requests.
- */
- private async shareKeyWithDevices(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- payload: IPayload,
- devicesByUser: Map<string, DeviceInfo[]>,
- errorDevices: IOlmDevice[],
- otkTimeout: number,
- failedServers?: string[],
- ): Promise<void> {
- const devicemap = await olmlib.ensureOlmSessionsForDevices(
- this.olmDevice,
- this.baseApis,
- devicesByUser,
- false,
- otkTimeout,
- failedServers,
- this.prefixedLogger,
- );
- this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
- await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
- }
-
- private async shareKeyWithOlmSessions(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- payload: IPayload,
- deviceMap: Map<string, Map<string, IOlmSessionResult>>,
- ): Promise<void> {
- const userDeviceMaps = this.splitDevices(deviceMap);
-
- for (let i = 0; i < userDeviceMaps.length; i++) {
- const taskDetail = `megolm keys for ${session.sessionId} (slice ${i + 1}/${userDeviceMaps.length})`;
- try {
- this.prefixedLogger.debug(
- `Sharing ${taskDetail}`,
- userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`),
- );
- await this.encryptAndSendKeysToDevices(session, key.chain_index, userDeviceMaps[i], payload);
- this.prefixedLogger.debug(`Shared ${taskDetail}`);
- } catch (e) {
- this.prefixedLogger.error(`Failed to share ${taskDetail}`);
- throw e;
- }
- }
- }
-
- /**
- * Notify devices that we weren't able to create olm sessions.
- *
- *
- *
- * @param failedDevices - the devices that we were unable to
- * create olm sessions for, as returned by shareKeyWithDevices
- */
- private async notifyFailedOlmDevices(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- failedDevices: IOlmDevice[],
- ): Promise<void> {
- this.prefixedLogger.debug(`Notifying ${failedDevices.length} devices we failed to create Olm sessions`);
-
- // mark the devices that failed as "handled" because we don't want to try
- // to claim a one-time-key for dead devices on every message.
- for (const { userId, deviceInfo } of failedDevices) {
- const deviceId = deviceInfo.deviceId;
-
- session.markSharedWithDevice(userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index);
- }
-
- const unnotifiedFailedDevices = await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
- this.prefixedLogger.debug(
- `Need to notify ${unnotifiedFailedDevices.length} failed devices which haven't been notified before`,
- );
- const blockedMap: MapWithDefault<string, Map<string, { device: IBlockedDevice }>> = new MapWithDefault(
- () => new Map(),
- );
- for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
- // we use a similar format to what
- // olmlib.ensureOlmSessionsForDevices returns, so that
- // we can use the same function to split
- blockedMap.getOrCreate(userId).set(deviceInfo.deviceId, {
- device: {
- code: "m.no_olm",
- reason: WITHHELD_MESSAGES["m.no_olm"],
- deviceInfo,
- },
- });
- }
-
- // send the notifications
- await this.notifyBlockedDevices(session, blockedMap);
- this.prefixedLogger.debug(
- `Notified ${unnotifiedFailedDevices.length} devices we failed to create Olm sessions`,
- );
- }
-
- /**
- * Notify blocked devices that they have been blocked.
- *
- *
- * @param devicesByUser - map from userid to device ID to blocked data
- */
- private async notifyBlockedDevices(
- session: OutboundSessionInfo,
- devicesByUser: Map<string, Map<string, { device: IBlockedDevice }>>,
- ): Promise<void> {
- const payload: IPayload = {
- room_id: this.roomId,
- session_id: session.sessionId,
- algorithm: olmlib.MEGOLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- };
-
- const userDeviceMaps = this.splitDevices(devicesByUser);
-
- for (let i = 0; i < userDeviceMaps.length; i++) {
- try {
- await this.sendBlockedNotificationsToDevices(session, userDeviceMaps[i], payload);
- this.prefixedLogger.log(
- `Completed blacklist notification for ${session.sessionId} ` +
- `(slice ${i + 1}/${userDeviceMaps.length})`,
- );
- } catch (e) {
- this.prefixedLogger.log(
- `blacklist notification for ${session.sessionId} ` +
- `(slice ${i + 1}/${userDeviceMaps.length}) failed`,
- );
-
- throw e;
- }
- }
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- * @returns A function that, when called, will stop the preparation
- */
- public prepareToEncrypt(room: Room): () => void {
- if (room.roomId !== this.roomId) {
- throw new Error("MegolmEncryption.prepareToEncrypt called on unexpected room");
- }
-
- if (this.encryptionPreparation != null) {
- // We're already preparing something, so don't do anything else.
- const elapsedTime = Date.now() - this.encryptionPreparation.startTime;
- this.prefixedLogger.debug(
- `Already started preparing to encrypt for this room ${elapsedTime}ms ago, skipping`,
- );
- return this.encryptionPreparation.cancel;
- }
-
- this.prefixedLogger.debug("Preparing to encrypt events");
-
- let cancelled = false;
- const isCancelled = (): boolean => cancelled;
-
- this.encryptionPreparation = {
- startTime: Date.now(),
- promise: (async (): Promise<void> => {
- try {
- // Attempt to enumerate the devices in room, and gracefully
- // handle cancellation if it occurs.
- const getDevicesResult = await this.getDevicesInRoom(room, false, isCancelled);
- if (getDevicesResult === null) return;
- const [devicesInRoom, blocked] = getDevicesResult;
-
- if (this.crypto.globalErrorOnUnknownDevices) {
- // Drop unknown devices for now. When the message gets sent, we'll
- // throw an error, but we'll still be prepared to send to the known
- // devices.
- this.removeUnknownDevices(devicesInRoom);
- }
-
- this.prefixedLogger.debug("Ensuring outbound megolm session");
- await this.ensureOutboundSession(room, devicesInRoom, blocked, true);
-
- this.prefixedLogger.debug("Ready to encrypt events");
- } catch (e) {
- this.prefixedLogger.error("Failed to prepare to encrypt events", e);
- } finally {
- delete this.encryptionPreparation;
- }
- })(),
-
- cancel: (): void => {
- // The caller has indicated that the process should be cancelled,
- // so tell the promise that we'd like to halt, and reset the preparation state.
- cancelled = true;
- delete this.encryptionPreparation;
- },
- };
-
- return this.encryptionPreparation.cancel;
- }
-
- /**
- * @param content - plaintext event content
- *
- * @returns Promise which resolves to the new event body
- */
- public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IMegolmEncryptedContent> {
- this.prefixedLogger.log("Starting to encrypt event");
-
- if (this.encryptionPreparation != null) {
- // If we started sending keys, wait for it to be done.
- // FIXME: check if we need to cancel
- // (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
- try {
- await this.encryptionPreparation.promise;
- } catch (e) {
- // ignore any errors -- if the preparation failed, we'll just
- // restart everything here
- }
- }
-
- /**
- * When using in-room messages and the room has encryption enabled,
- * clients should ensure that encryption does not hinder the verification.
- */
- const forceDistributeToUnverified = this.isVerificationEvent(eventType, content);
- const [devicesInRoom, blocked] = await this.getDevicesInRoom(room, forceDistributeToUnverified);
-
- // check if any of these devices are not yet known to the user.
- // if so, warn the user so they can verify or ignore.
- if (this.crypto.globalErrorOnUnknownDevices) {
- this.checkForUnknownDevices(devicesInRoom);
- }
-
- const session = await this.ensureOutboundSession(room, devicesInRoom, blocked);
- const payloadJson = {
- room_id: this.roomId,
- type: eventType,
- content: content,
- };
-
- const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson));
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.MEGOLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: ciphertext,
- session_id: session.sessionId,
- // Include our device ID so that recipients can send us a
- // m.new_device message if they don't have our session key.
- // XXX: Do we still need this now that m.new_device messages
- // no longer exist since #483?
- device_id: this.deviceId,
- };
-
- session.useCount++;
- return encryptedContent;
- }
-
- private isVerificationEvent(eventType: string, content: IContent): boolean {
- switch (eventType) {
- case EventType.KeyVerificationCancel:
- case EventType.KeyVerificationDone:
- case EventType.KeyVerificationMac:
- case EventType.KeyVerificationStart:
- case EventType.KeyVerificationKey:
- case EventType.KeyVerificationReady:
- case EventType.KeyVerificationAccept: {
- return true;
- }
- case EventType.RoomMessage: {
- return content["msgtype"] === MsgType.KeyVerificationRequest;
- }
- default: {
- return false;
- }
- }
- }
-
- /**
- * Forces the current outbound group session to be discarded such
- * that another one will be created next time an event is sent.
- *
- * This should not normally be necessary.
- */
- public forceDiscardSession(): void {
- this.setupPromise = this.setupPromise.then(() => null);
- }
-
- /**
- * Checks the devices we're about to send to and see if any are entirely
- * unknown to the user. If so, warn the user, and mark them as known to
- * give the user a chance to go verify them before re-sending this message.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- */
- private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
- const unknownDevices: MapWithDefault<string, Map<string, DeviceInfo>> = new MapWithDefault(() => new Map());
-
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, device] of userDevices) {
- if (device.isUnverified() && !device.isKnown()) {
- unknownDevices.getOrCreate(userId).set(deviceId, device);
- }
- }
- }
-
- if (unknownDevices.size) {
- // it'd be kind to pass unknownDevices up to the user in this error
- throw new UnknownDeviceError(
- "This room contains unknown devices which have not been verified. " +
- "We strongly recommend you verify them before continuing.",
- unknownDevices,
- );
- }
- }
-
- /**
- * Remove unknown devices from a set of devices. The devicesInRoom parameter
- * will be modified.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- */
- private removeUnknownDevices(devicesInRoom: DeviceInfoMap): void {
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, device] of userDevices) {
- if (device.isUnverified() && !device.isKnown()) {
- userDevices.delete(deviceId);
- }
- }
-
- if (userDevices.size === 0) {
- devicesInRoom.delete(userId);
- }
- }
- }
-
- /**
- * Get the list of unblocked devices for all users in the room
- *
- * @param forceDistributeToUnverified - if set to true will include the unverified devices
- * even if setting is set to block them (useful for verification)
- * @param isCancelled - will cause the procedure to abort early if and when it starts
- * returning `true`. If omitted, cancellation won't happen.
- *
- * @returns Promise which resolves to `null`, or an array whose
- * first element is a {@link DeviceInfoMap} indicating
- * the devices that messages should be encrypted to, and whose second
- * element is a map from userId to deviceId to data indicating the devices
- * that are in the room but that have been blocked.
- * If `isCancelled` is provided and returns `true` while processing, `null`
- * will be returned.
- * If `isCancelled` is not provided, the Promise will never resolve to `null`.
- */
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified?: boolean,
- ): Promise<[DeviceInfoMap, BlockedMap]>;
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified?: boolean,
- isCancelled?: () => boolean,
- ): Promise<null | [DeviceInfoMap, BlockedMap]>;
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified = false,
- isCancelled?: () => boolean,
- ): Promise<null | [DeviceInfoMap, BlockedMap]> {
- const members = await room.getEncryptionTargetMembers();
- this.prefixedLogger.debug(
- `Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`,
- members.map((u) => `${u.userId} (${u.membership})`),
- );
-
- const roomMembers = members.map(function (u) {
- return u.userId;
- });
-
- // The global value is treated as a default for when rooms don't specify a value.
- let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices;
- const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
- if (typeof isRoomBlacklisting === "boolean") {
- isBlacklisting = isRoomBlacklisting;
- }
-
- // We are happy to use a cached version here: we assume that if we already
- // have a list of the user's devices, then we already share an e2e room
- // with them, which means that they will have announced any new devices via
- // device_lists in their /sync response. This cache should then be maintained
- // using all the device_lists changes and left fields.
- // See https://github.com/vector-im/element-web/issues/2305 for details.
- const devices = await this.crypto.downloadKeys(roomMembers, false);
-
- if (isCancelled?.() === true) {
- return null;
- }
-
- const blocked = new MapWithDefault<string, Map<string, IBlockedDevice>>(() => new Map());
- // remove any blocked devices
- for (const [userId, userDevices] of devices) {
- for (const [deviceId, userDevice] of userDevices) {
- // Yield prior to checking each device so that we don't block
- // updating/rendering for too long.
- // See https://github.com/vector-im/element-web/issues/21612
- if (isCancelled !== undefined) await immediate();
- if (isCancelled?.() === true) return null;
- const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
-
- if (
- userDevice.isBlocked() ||
- (!deviceTrust.isVerified() && isBlacklisting && !forceDistributeToUnverified)
- ) {
- const blockedDevices = blocked.getOrCreate(userId);
- const isBlocked = userDevice.isBlocked();
- blockedDevices.set(deviceId, {
- code: isBlocked ? "m.blacklisted" : "m.unverified",
- reason: WITHHELD_MESSAGES[isBlocked ? "m.blacklisted" : "m.unverified"],
- deviceInfo: userDevice,
- });
- userDevices.delete(deviceId);
- }
- }
- }
-
- return [devices, blocked];
- }
-}
-
-/**
- * Megolm decryption implementation
- *
- * @param params - parameters, as per {@link DecryptionAlgorithm}
- */
-export class MegolmDecryption extends DecryptionAlgorithm {
- // events which we couldn't decrypt due to unknown sessions /
- // indexes, or which we could only decrypt with untrusted keys:
- // map from senderKey|sessionId to Set of MatrixEvents
- private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
-
- // this gets stubbed out by the unit tests.
- private olmlib = olmlib;
-
- protected readonly roomId: string;
- private readonly prefixedLogger: PrefixedLogger;
-
- public constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
- super(params);
- this.roomId = params.roomId;
- this.prefixedLogger = logger.withPrefix(`[${this.roomId} decryption]`);
- }
-
- /**
- * returns a promise which resolves to a
- * {@link EventDecryptionResult} once we have finished
- * decrypting, or rejects with an `algorithms.DecryptionError` if there is a
- * problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- const content = event.getWireContent();
-
- if (!content.sender_key || !content.session_id || !content.ciphertext) {
- throw new DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input");
- }
-
- // we add the event to the pending list *before* we start decryption.
- //
- // then, if the key turns up while decryption is in progress (and
- // decryption fails), we will schedule a retry.
- // (fixes https://github.com/vector-im/element-web/issues/5001)
- this.addEventToPendingList(event);
-
- let res: IDecryptedGroupMessage | null;
- try {
- res = await this.olmDevice.decryptGroupMessage(
- event.getRoomId()!,
- content.sender_key,
- content.session_id,
- content.ciphertext,
- event.getId()!,
- event.getTs(),
- );
- } catch (e) {
- if ((<Error>e).name === "DecryptionError") {
- // re-throw decryption errors as-is
- throw e;
- }
-
- let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
-
- if ((<MatrixError>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX") {
- this.requestKeysForEvent(event);
-
- errorCode = "OLM_UNKNOWN_MESSAGE_INDEX";
- }
-
- throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", {
- session: content.sender_key + "|" + content.session_id,
- });
- }
-
- if (res === null) {
- // We've got a message for a session we don't have.
- // try and get the missing key from the backup first
- this.crypto.backupManager.queryKeyBackupRateLimited(event.getRoomId(), content.session_id).catch(() => {});
-
- // (XXX: We might actually have received this key since we started
- // decrypting, in which case we'll have scheduled a retry, and this
- // request will be redundant. We could probably check to see if the
- // event is still in the pending list; if not, a retry will have been
- // scheduled, so we needn't send out the request here.)
- this.requestKeysForEvent(event);
-
- // See if there was a problem with the olm session at the time the
- // event was sent. Use a fuzz factor of 2 minutes.
- const problem = await this.olmDevice.sessionMayHaveProblems(content.sender_key, event.getTs() - 120000);
- if (problem) {
- this.prefixedLogger.info(
- `When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` +
- `recent session problem with that sender:`,
- problem,
- );
- let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
- if (problem.fixed) {
- problemDescription += " Trying to create a new secure channel and re-requesting the keys.";
- }
- throw new DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, {
- session: content.sender_key + "|" + content.session_id,
- });
- }
-
- throw new DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- "The sender's device has not sent us the keys for this message.",
- {
- session: content.sender_key + "|" + content.session_id,
- },
- );
- }
-
- // Success. We can remove the event from the pending list, if
- // that hasn't already happened. However, if the event was
- // decrypted with an untrusted key, leave it on the pending
- // list so it will be retried if we find a trusted key later.
- if (!res.untrusted) {
- this.removeEventFromPendingList(event);
- }
-
- const payload = JSON.parse(res.result);
-
- // belt-and-braces check that the room id matches that indicated by the HS
- // (this is somewhat redundant, since the megolm session is scoped to the
- // room, so neither the sender nor a MITM can lie about the room_id).
- if (payload.room_id !== event.getRoomId()) {
- throw new DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id);
- }
-
- return {
- clearEvent: payload,
- senderCurve25519Key: res.senderKey,
- claimedEd25519Key: res.keysClaimed.ed25519,
- forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
- untrusted: res.untrusted,
- };
- }
-
- private requestKeysForEvent(event: MatrixEvent): void {
- const wireContent = event.getWireContent();
-
- const recipients = event.getKeyRequestRecipients(this.userId);
-
- this.crypto.requestRoomKey(
- {
- room_id: event.getRoomId()!,
- algorithm: wireContent.algorithm,
- sender_key: wireContent.sender_key,
- session_id: wireContent.session_id,
- },
- recipients,
- );
- }
-
- /**
- * Add an event to the list of those awaiting their session keys.
- *
- * @internal
- *
- */
- private addEventToPendingList(event: MatrixEvent): void {
- const content = event.getWireContent();
- const senderKey = content.sender_key;
- const sessionId = content.session_id;
- if (!this.pendingEvents.has(senderKey)) {
- this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
- }
- const senderPendingEvents = this.pendingEvents.get(senderKey)!;
- if (!senderPendingEvents.has(sessionId)) {
- senderPendingEvents.set(sessionId, new Set());
- }
- senderPendingEvents.get(sessionId)?.add(event);
- }
-
- /**
- * Remove an event from the list of those awaiting their session keys.
- *
- * @internal
- *
- */
- private removeEventFromPendingList(event: MatrixEvent): void {
- const content = event.getWireContent();
- const senderKey = content.sender_key;
- const sessionId = content.session_id;
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- const pendingEvents = senderPendingEvents?.get(sessionId);
- if (!pendingEvents) {
- return;
- }
-
- pendingEvents.delete(event);
- if (pendingEvents.size === 0) {
- senderPendingEvents!.delete(sessionId);
- }
- if (senderPendingEvents!.size === 0) {
- this.pendingEvents.delete(senderKey);
- }
- }
-
- /**
- * Parse a RoomKey out of an `m.room_key` event.
- *
- * @param event - the event containing the room key.
- *
- * @returns The `RoomKey` if it could be successfully parsed out of the
- * event.
- *
- * @internal
- *
- */
- private roomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
- const senderKey = event.getSenderKey()!;
- const content = event.getContent<Partial<IMessage["content"]>>();
- const extraSessionData: OlmGroupSessionExtraData = {};
-
- if (!content.room_id || !content.session_key || !content.session_id || !content.algorithm) {
- this.prefixedLogger.error("key event is missing fields");
- return;
- }
-
- if (!olmlib.isOlmEncrypted(event)) {
- this.prefixedLogger.error("key event not properly encrypted");
- return;
- }
-
- if (content["org.matrix.msc3061.shared_history"]) {
- extraSessionData.sharedHistory = true;
- }
-
- const roomKey: RoomKey = {
- senderKey: senderKey,
- sessionId: content.session_id,
- sessionKey: content.session_key,
- extraSessionData,
- exportFormat: false,
- roomId: content.room_id,
- algorithm: content.algorithm,
- forwardingKeyChain: [],
- keysClaimed: event.getKeysClaimed(),
- };
-
- return roomKey;
- }
-
- /**
- * Parse a RoomKey out of an `m.forwarded_room_key` event.
- *
- * @param event - the event containing the forwarded room key.
- *
- * @returns The `RoomKey` if it could be successfully parsed out of the
- * event.
- *
- * @internal
- *
- */
- private forwardedRoomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
- // the properties in m.forwarded_room_key are a superset of those in m.room_key, so
- // start by parsing the m.room_key fields.
- const roomKey = this.roomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- const senderKey = event.getSenderKey()!;
- const content = event.getContent<Partial<IMessage["content"]>>();
-
- const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
-
- // We received this to-device event from event.getSenderKey(), but the original
- // creator of the room key is claimed in the content.
- const claimedCurve25519Key = content.sender_key;
- const claimedEd25519Key = content.sender_claimed_ed25519_key;
-
- let forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain)
- ? content.forwarding_curve25519_key_chain
- : [];
-
- // copy content before we modify it
- forwardingKeyChain = forwardingKeyChain.slice();
- forwardingKeyChain.push(senderKey);
-
- // Check if we have all the fields we need.
- if (senderKeyUser !== event.getSender()) {
- this.prefixedLogger.error("sending device does not belong to the user it claims to be from");
- return;
- }
-
- if (!claimedCurve25519Key) {
- this.prefixedLogger.error("forwarded_room_key event is missing sender_key field");
- return;
- }
-
- if (!claimedEd25519Key) {
- this.prefixedLogger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`);
- return;
- }
-
- const keysClaimed = {
- ed25519: claimedEd25519Key,
- };
-
- // FIXME: We're reusing the same field to track both:
- //
- // 1. The Olm identity we've received this room key from.
- // 2. The Olm identity deduced (in the trusted case) or claiming (in the
- // untrusted case) to be the original creator of this room key.
- //
- // We now overwrite the value tracking usage 1 with the value tracking usage 2.
- roomKey.senderKey = claimedCurve25519Key;
- // Replace our keysClaimed as well.
- roomKey.keysClaimed = keysClaimed;
- roomKey.exportFormat = true;
- roomKey.forwardingKeyChain = forwardingKeyChain;
- // forwarded keys are always untrusted
- roomKey.extraSessionData.untrusted = true;
-
- return roomKey;
- }
-
- /**
- * Determine if we should accept the forwarded room key that was found in the given
- * event.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @returns promise that will resolve to a boolean telling us if it's ok to
- * accept the given forwarded room key.
- *
- * @internal
- *
- */
- private async shouldAcceptForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
- const senderKey = event.getSenderKey()!;
-
- const sendingDevice =
- this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey) ?? undefined;
- const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
-
- // Using the plaintext sender here is fine since we checked that the
- // sender matches to the user id in the device keys when this event was
- // originally decrypted. This can obviously only happen if the device
- // keys have been downloaded, but if they haven't the
- // `deviceTrust.isVerified()` flag would be false as well.
- //
- // It would still be far nicer if the `sendingDevice` had a user ID
- // attached to it that went through signature checks.
- const fromUs = event.getSender() === this.baseApis.getUserId();
- const keyFromOurVerifiedDevice = deviceTrust.isVerified() && fromUs;
- const weRequested = await this.wasRoomKeyRequested(event, roomKey);
- const fromInviter = this.wasRoomKeyForwardedByInviter(event, roomKey);
- const sharedAsHistory = this.wasRoomKeyForwardedAsHistory(roomKey);
-
- return (weRequested && keyFromOurVerifiedDevice) || (fromInviter && sharedAsHistory);
- }
-
- /**
- * Did we ever request the given room key from the event sender and its
- * accompanying device.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @internal
- *
- */
- private async wasRoomKeyRequested(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
- // We send the `m.room_key_request` out as a wildcard to-device request,
- // otherwise we would have to duplicate the same content for each
- // device. This is why we need to pass in "*" as the device id here.
- const outgoingRequests = await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
- event.getSender()!,
- "*",
- [RoomKeyRequestState.Sent],
- );
-
- return outgoingRequests.some(
- (req) => req.requestBody.room_id === roomKey.roomId && req.requestBody.session_id === roomKey.sessionId,
- );
- }
-
- private wasRoomKeyForwardedByInviter(event: MatrixEvent, roomKey: RoomKey): boolean {
- // TODO: This is supposed to have a time limit. We should only accept
- // such keys if we happen to receive them for a recently joined room.
- const room = this.baseApis.getRoom(roomKey.roomId);
- const senderKey = event.getSenderKey();
-
- if (!senderKey) {
- return false;
- }
-
- const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
-
- if (!senderKeyUser) {
- return false;
- }
-
- const memberEvent = room?.getMember(this.userId)?.events.member;
- const fromInviter =
- memberEvent?.getSender() === senderKeyUser ||
- (memberEvent?.getUnsigned()?.prev_sender === senderKeyUser &&
- memberEvent?.getPrevContent()?.membership === "invite");
-
- if (room && fromInviter) {
- return true;
- } else {
- return false;
- }
- }
-
- private wasRoomKeyForwardedAsHistory(roomKey: RoomKey): boolean {
- const room = this.baseApis.getRoom(roomKey.roomId);
-
- // If the key is not for a known room, then something fishy is going on,
- // so we reject the key out of caution. In practice, this is a bit moot
- // because we'll only accept shared_history forwarded by the inviter, and
- // we won't know who was the inviter for an unknown room, so we'll reject
- // it anyway.
- if (room && roomKey.extraSessionData.sharedHistory) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Check if a forwarded room key should be parked.
- *
- * A forwarded room key should be parked if it's a key for a room we're not
- * in. We park the forwarded room key in case *this sender* invites us to
- * that room later.
- */
- private shouldParkForwardedKey(roomKey: RoomKey): boolean {
- const room = this.baseApis.getRoom(roomKey.roomId);
-
- if (!room && roomKey.extraSessionData.sharedHistory) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Park the given room key to our store.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @internal
- *
- */
- private async parkForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<void> {
- const parkedData = {
- senderId: event.getSender()!,
- senderKey: roomKey.senderKey,
- sessionId: roomKey.sessionId,
- sessionKey: roomKey.sessionKey,
- keysClaimed: roomKey.keysClaimed,
- forwardingCurve25519KeyChain: roomKey.forwardingKeyChain,
- };
- await this.crypto.cryptoStore.doTxn(
- "readwrite",
- ["parked_shared_history"],
- (txn) => this.crypto.cryptoStore.addParkedSharedHistory(roomKey.roomId, parkedData, txn),
- logger.withPrefix("[addParkedSharedHistory]"),
- );
- }
-
- /**
- * Add the given room key to our store.
- *
- * @param roomKey - The room key that should be added to the store.
- *
- * @internal
- *
- */
- private async addRoomKey(roomKey: RoomKey): Promise<void> {
- try {
- await this.olmDevice.addInboundGroupSession(
- roomKey.roomId,
- roomKey.senderKey,
- roomKey.forwardingKeyChain,
- roomKey.sessionId,
- roomKey.sessionKey,
- roomKey.keysClaimed,
- roomKey.exportFormat,
- roomKey.extraSessionData,
- );
-
- // have another go at decrypting events sent with this session.
- if (await this.retryDecryption(roomKey.senderKey, roomKey.sessionId, !roomKey.extraSessionData.untrusted)) {
- // cancel any outstanding room key requests for this session.
- // Only do this if we managed to decrypt every message in the
- // session, because if we didn't, we leave the other key
- // requests in the hopes that someone sends us a key that
- // includes an earlier index.
- this.crypto.cancelRoomKeyRequest({
- algorithm: roomKey.algorithm,
- room_id: roomKey.roomId,
- session_id: roomKey.sessionId,
- sender_key: roomKey.senderKey,
- });
- }
-
- // don't wait for the keys to be backed up for the server
- await this.crypto.backupManager.backupGroupSession(roomKey.senderKey, roomKey.sessionId);
- } catch (e) {
- this.prefixedLogger.error(`Error handling m.room_key_event: ${e}`);
- }
- }
-
- /**
- * Handle room keys that have been forwarded to us as an
- * `m.forwarded_room_key` event.
- *
- * Forwarded room keys need special handling since we have no way of knowing
- * who the original creator of the room key was. This naturally means that
- * forwarded room keys are always untrusted and should only be accepted in
- * some cases.
- *
- * @param event - An `m.forwarded_room_key` event.
- *
- * @internal
- *
- */
- private async onForwardedRoomKey(event: MatrixEvent): Promise<void> {
- const roomKey = this.forwardedRoomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- if (await this.shouldAcceptForwardedKey(event, roomKey)) {
- await this.addRoomKey(roomKey);
- } else if (this.shouldParkForwardedKey(roomKey)) {
- await this.parkForwardedKey(event, roomKey);
- }
- }
-
- public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
- if (event.getType() == "m.forwarded_room_key") {
- await this.onForwardedRoomKey(event);
- } else {
- const roomKey = this.roomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- await this.addRoomKey(roomKey);
- }
- }
-
- /**
- * @param event - key event
- */
- public async onRoomKeyWithheldEvent(event: MatrixEvent): Promise<void> {
- const content = event.getContent();
- const senderKey = content.sender_key;
-
- if (content.code === "m.no_olm") {
- await this.onNoOlmWithheldEvent(event);
- } else if (content.code === "m.unavailable") {
- // this simply means that the other device didn't have the key, which isn't very useful information. Don't
- // record it in the storage
- } else {
- await this.olmDevice.addInboundGroupSessionWithheld(
- content.room_id,
- senderKey,
- content.session_id,
- content.code,
- content.reason,
- );
- }
-
- // Having recorded the problem, retry decryption on any affected messages.
- // It's unlikely we'll be able to decrypt sucessfully now, but this will
- // update the error message.
- //
- if (content.session_id) {
- await this.retryDecryption(senderKey, content.session_id);
- } else {
- // no_olm messages aren't specific to a given megolm session, so
- // we trigger retrying decryption for all the messages from the sender's
- // key, so that we can update the error message to indicate the olm
- // session problem.
- await this.retryDecryptionFromSender(senderKey);
- }
- }
-
- private async onNoOlmWithheldEvent(event: MatrixEvent): Promise<void> {
- const content = event.getContent();
- const senderKey = content.sender_key;
- const sender = event.getSender()!;
- this.prefixedLogger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`);
- // if the sender says that they haven't been able to establish an olm
- // session, let's proactively establish one
-
- if (await this.olmDevice.getSessionIdForDevice(senderKey)) {
- // a session has already been established, so we don't need to
- // create a new one.
- this.prefixedLogger.debug("New session already created. Not creating a new one.");
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
- return;
- }
- let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
- if (!device) {
- // if we don't know about the device, fetch the user's devices again
- // and retry before giving up
- await this.crypto.downloadKeys([sender], false);
- device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
- if (!device) {
- this.prefixedLogger.info(
- "Couldn't find device for identity key " + senderKey + ": not establishing session",
- );
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false);
- return;
- }
- }
-
- // XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
-
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[sender, [device]]]), false);
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- sender,
- device,
- { type: "m.dummy" },
- );
-
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
- );
- }
-
- public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
- const body = keyRequest.requestBody;
-
- return this.olmDevice.hasInboundSessionKeys(
- body.room_id,
- body.sender_key,
- body.session_id,
- // TODO: ratchet index
- );
- }
-
- public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
- const userId = keyRequest.userId;
- const deviceId = keyRequest.deviceId;
- const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
- const body = keyRequest.requestBody;
-
- // XXX: switch this to use encryptAndSendToDevices()?
-
- this.olmlib
- .ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [deviceInfo]]]))
- .then((devicemap) => {
- const olmSessionResult = devicemap.get(userId)?.get(deviceId);
- if (!olmSessionResult?.sessionId) {
- // no session with this device, probably because there
- // were no one-time keys.
- //
- // ensureOlmSessionsForUsers has already done the logging,
- // so just skip it.
- return null;
- }
-
- this.prefixedLogger.log(
- "sharing keys for session " +
- body.sender_key +
- "|" +
- body.session_id +
- " with device " +
- userId +
- ":" +
- deviceId,
- );
-
- return this.buildKeyForwardingMessage(body.room_id, body.sender_key, body.session_id);
- })
- .then((payload) => {
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
-
- return this.olmlib
- .encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- userId,
- deviceInfo,
- payload!,
- )
- .then(() => {
- // TODO: retries
- return this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[userId, new Map([[deviceId, encryptedContent]])]]),
- );
- });
- });
- }
-
- private async buildKeyForwardingMessage(
- roomId: string,
- senderKey: string,
- sessionId: string,
- ): Promise<IKeyForwardingMessage> {
- const key = await this.olmDevice.getInboundGroupSessionKey(roomId, senderKey, sessionId);
-
- return {
- type: "m.forwarded_room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": roomId,
- "sender_key": senderKey,
- "sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!,
- "session_id": sessionId,
- "session_key": key!.key,
- "chain_index": key!.chain_index,
- "forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain,
- "org.matrix.msc3061.shared_history": key!.shared_history || false,
- },
- };
- }
-
- /**
- * @param untrusted - whether the key should be considered as untrusted
- * @param source - where the key came from
- */
- public importRoomKey(
- session: IMegolmSessionData,
- { untrusted, source }: { untrusted?: boolean; source?: string } = {},
- ): Promise<void> {
- const extraSessionData: OlmGroupSessionExtraData = {};
- if (untrusted || session.untrusted) {
- extraSessionData.untrusted = true;
- }
- if (session["org.matrix.msc3061.shared_history"]) {
- extraSessionData.sharedHistory = true;
- }
- return this.olmDevice
- .addInboundGroupSession(
- session.room_id,
- session.sender_key,
- session.forwarding_curve25519_key_chain,
- session.session_id,
- session.session_key,
- session.sender_claimed_keys,
- true,
- extraSessionData,
- )
- .then(() => {
- if (source !== "backup") {
- // don't wait for it to complete
- this.crypto.backupManager.backupGroupSession(session.sender_key, session.session_id).catch((e) => {
- // This throws if the upload failed, but this is fine
- // since it will have written it to the db and will retry.
- this.prefixedLogger.log("Failed to back up megolm session", e);
- });
- }
- // have another go at decrypting events sent with this session.
- this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
- });
- }
-
- /**
- * Have another go at decrypting events after we receive a key. Resolves once
- * decryption has been re-attempted on all events.
- *
- * @internal
- * @param forceRedecryptIfUntrusted - whether messages that were already
- * successfully decrypted using untrusted keys should be re-decrypted
- *
- * @returns whether all messages were successfully
- * decrypted with trusted keys
- */
- private async retryDecryption(
- senderKey: string,
- sessionId: string,
- forceRedecryptIfUntrusted?: boolean,
- ): Promise<boolean> {
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- if (!senderPendingEvents) {
- return true;
- }
-
- const pending = senderPendingEvents.get(sessionId);
- if (!pending) {
- return true;
- }
-
- const pendingList = [...pending];
- this.prefixedLogger.debug(
- "Retrying decryption on events:",
- pendingList.map((e) => `${e.getId()}`),
- );
-
- await Promise.all(
- pendingList.map(async (ev) => {
- try {
- await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
- } catch (e) {
- // don't die if something goes wrong
- }
- }),
- );
-
- // If decrypted successfully with trusted keys, they'll have
- // been removed from pendingEvents
- return !this.pendingEvents.get(senderKey)?.has(sessionId);
- }
-
- public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- if (!senderPendingEvents) {
- return true;
- }
-
- this.pendingEvents.delete(senderKey);
-
- await Promise.all(
- [...senderPendingEvents].map(async ([_sessionId, pending]) => {
- await Promise.all(
- [...pending].map(async (ev) => {
- try {
- await ev.attemptDecryption(this.crypto);
- } catch (e) {
- // don't die if something goes wrong
- }
- }),
- );
- }),
- );
-
- return !this.pendingEvents.has(senderKey);
- }
-
- public async sendSharedHistoryInboundSessions(devicesByUser: Map<string, DeviceInfo[]>): Promise<void> {
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
-
- const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId);
- this.prefixedLogger.log(
- `Sharing history in with users ${Array.from(devicesByUser.keys())}`,
- sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`),
- );
- for (const [senderKey, sessionId] of sharedHistorySessions) {
- const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
-
- // FIXME: use encryptAndSendToDevices() rather than duplicating it here.
- const promises: Promise<unknown>[] = [];
- const contentMap: Map<string, Map<string, IEncryptedContent>> = new Map();
- for (const [userId, devices] of devicesByUser) {
- const deviceMessages = new Map();
- contentMap.set(userId, deviceMessages);
- for (const deviceInfo of devices) {
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- deviceMessages.set(deviceInfo.deviceId, encryptedContent);
- promises.push(
- olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- userId,
- deviceInfo,
- payload,
- ),
- );
- }
- }
- await Promise.all(promises);
-
- // prune out any devices that encryptMessageForDevice could not encrypt for,
- // in which case it will have just not added anything to the ciphertext object.
- // There's no point sending messages to devices if we couldn't encrypt to them,
- // since that's effectively a blank message.
- for (const [userId, deviceMessages] of contentMap) {
- for (const [deviceId, content] of deviceMessages) {
- if (!hasCiphertext(content)) {
- this.prefixedLogger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning");
- deviceMessages.delete(deviceId);
- }
- }
- // No devices left for that user? Strip that too.
- if (deviceMessages.size === 0) {
- this.prefixedLogger.log("Pruned all devices for user " + userId);
- contentMap.delete(userId);
- }
- }
-
- // Is there anything left?
- if (contentMap.size === 0) {
- this.prefixedLogger.log("No users left to send to: aborting");
- return;
- }
-
- await this.baseApis.sendToDevice("m.room.encrypted", contentMap);
- }
- }
-}
-
-const PROBLEM_DESCRIPTIONS = {
- no_olm: "The sender was unable to establish a secure channel.",
- unknown: "The secure channel with the sender was corrupted.",
-};
-
-registerAlgorithm(olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption);
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts
deleted file mode 100644
index 1a79554..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Defines m.olm encryption/decryption
- */
-
-import type { IEventDecryptionResult } from "../../@types/crypto";
-import { logger } from "../../logger";
-import * as olmlib from "../olmlib";
-import { DeviceInfo } from "../deviceinfo";
-import { DecryptionAlgorithm, DecryptionError, EncryptionAlgorithm, registerAlgorithm } from "./base";
-import { Room } from "../../models/room";
-import { IContent, MatrixEvent } from "../../models/event";
-import { IEncryptedContent, IOlmEncryptedContent } from "../index";
-import { IInboundSession } from "../OlmDevice";
-
-const DeviceVerification = DeviceInfo.DeviceVerification;
-
-export interface IMessage {
- type: number;
- body: string;
-}
-
-/**
- * Olm encryption implementation
- *
- * @param params - parameters, as per {@link EncryptionAlgorithm}
- */
-class OlmEncryption extends EncryptionAlgorithm {
- private sessionPrepared = false;
- private prepPromise: Promise<void> | null = null;
-
- /**
- * @internal
-
- * @param roomMembers - list of currently-joined users in the room
- * @returns Promise which resolves when setup is complete
- */
- private ensureSession(roomMembers: string[]): Promise<void> {
- if (this.prepPromise) {
- // prep already in progress
- return this.prepPromise;
- }
-
- if (this.sessionPrepared) {
- // prep already done
- return Promise.resolve();
- }
-
- this.prepPromise = this.crypto
- .downloadKeys(roomMembers)
- .then(() => {
- return this.crypto.ensureOlmSessionsForUsers(roomMembers);
- })
- .then(() => {
- this.sessionPrepared = true;
- })
- .finally(() => {
- this.prepPromise = null;
- });
-
- return this.prepPromise;
- }
-
- /**
- * @param content - plaintext event content
- *
- * @returns Promise which resolves to the new event body
- */
- public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IOlmEncryptedContent> {
- // pick the list of recipients based on the membership list.
- //
- // TODO: there is a race condition here! What if a new user turns up
- // just as you are sending a secret message?
-
- const members = await room.getEncryptionTargetMembers();
-
- const users = members.map(function (u) {
- return u.userId;
- });
-
- await this.ensureSession(users);
-
- const payloadFields = {
- room_id: room.roomId,
- type: eventType,
- content: content,
- };
-
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- };
-
- const promises: Promise<void>[] = [];
-
- for (const userId of users) {
- const devices = this.crypto.getStoredDevicesForUser(userId) || [];
-
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother sending to ourself
- continue;
- }
- if (deviceInfo.verified == DeviceVerification.BLOCKED) {
- // don't bother setting up sessions with blocked users
- continue;
- }
-
- promises.push(
- olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- deviceInfo,
- payloadFields,
- ),
- );
- }
- }
-
- return Promise.all(promises).then(() => encryptedContent);
- }
-}
-
-/**
- * Olm decryption implementation
- *
- * @param params - parameters, as per {@link DecryptionAlgorithm}
- */
-class OlmDecryption extends DecryptionAlgorithm {
- /**
- * returns a promise which resolves to a
- * {@link EventDecryptionResult} once we have finished
- * decrypting. Rejects with an `algorithms.DecryptionError` if there is a
- * problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- const content = event.getWireContent();
- const deviceKey = content.sender_key;
- const ciphertext = content.ciphertext;
-
- if (!ciphertext) {
- throw new DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext");
- }
-
- if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) {
- throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients");
- }
- const message = ciphertext[this.olmDevice.deviceCurve25519Key!];
- let payloadString: string;
-
- try {
- payloadString = await this.decryptMessage(deviceKey, message);
- } catch (e) {
- throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", {
- sender: deviceKey,
- err: e as Error,
- });
- }
-
- const payload = JSON.parse(payloadString);
-
- // check that we were the intended recipient, to avoid unknown-key attack
- // https://github.com/vector-im/vector-web/issues/2483
- if (payload.recipient != this.userId) {
- throw new DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient);
- }
-
- if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
- throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", {
- intended: payload.recipient_keys.ed25519,
- our_key: this.olmDevice.deviceEd25519Key!,
- });
- }
-
- // check that the device that encrypted the event belongs to the user
- // that the event claims it's from. We need to make sure that our
- // device list is up-to-date. If the device is unknown, we can only
- // assume that the device logged out. Some event handlers, such as
- // secret sharing, may be more strict and reject events that come from
- // unknown devices.
- await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
- const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
- if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
- throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), {
- real_sender: senderKeyUser,
- });
- }
-
- // check that the original sender matches what the homeserver told us, to
- // avoid people masquerading as others.
- // (this check is also provided via the sender's embedded ed25519 key,
- // which is checked elsewhere).
- if (payload.sender != event.getSender()) {
- throw new DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, {
- reported_sender: event.getSender()!,
- });
- }
-
- // Olm events intended for a room have a room_id.
- if (payload.room_id !== event.getRoomId()) {
- throw new DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, {
- reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED",
- });
- }
-
- const claimedKeys = payload.keys || {};
-
- return {
- clearEvent: payload,
- senderCurve25519Key: deviceKey,
- claimedEd25519Key: claimedKeys.ed25519 || null,
- };
- }
-
- /**
- * Attempt to decrypt an Olm message
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key of the sender
- * @param message - message object, with 'type' and 'body' fields
- *
- * @returns payload, if decrypted successfully.
- */
- private decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
- // This is a wrapper that serialises decryptions of prekey messages, because
- // otherwise we race between deciding we have no active sessions for the message
- // and creating a new one, which we can only do once because it removes the OTK.
- if (message.type !== 0) {
- // not a prekey message: we can safely just try & decrypt it
- return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
- } else {
- const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
- return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
- });
- // we want the error, but don't propagate it to the next decryption
- this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
- return myPromise;
- }
- }
-
- private async reallyDecryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
- const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
-
- // try each session in turn.
- const decryptionErrors: Record<string, string> = {};
- for (const sessionId of sessionIds) {
- try {
- const payload = await this.olmDevice.decryptMessage(
- theirDeviceIdentityKey,
- sessionId,
- message.type,
- message.body,
- );
- logger.log("Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId);
- return payload;
- } catch (e) {
- const foundSession = await this.olmDevice.matchesSession(
- theirDeviceIdentityKey,
- sessionId,
- message.type,
- message.body,
- );
-
- if (foundSession) {
- // decryption failed, but it was a prekey message matching this
- // session, so it should have worked.
- throw new Error(
- "Error decrypting prekey message with existing session id " +
- sessionId +
- ": " +
- (<Error>e).message,
- );
- }
-
- // otherwise it's probably a message for another session; carry on, but
- // keep a record of the error
- decryptionErrors[sessionId] = (<Error>e).message;
- }
- }
-
- if (message.type !== 0) {
- // not a prekey message, so it should have matched an existing session, but it
- // didn't work.
-
- if (sessionIds.length === 0) {
- throw new Error("No existing sessions");
- }
-
- throw new Error(
- "Error decrypting non-prekey message with existing sessions: " + JSON.stringify(decryptionErrors),
- );
- }
-
- // prekey message which doesn't match any existing sessions: make a new
- // session.
-
- let res: IInboundSession;
- try {
- res = await this.olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body);
- } catch (e) {
- decryptionErrors["(new)"] = (<Error>e).message;
- throw new Error("Error decrypting prekey message: " + JSON.stringify(decryptionErrors));
- }
-
- logger.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey);
- return res.payload;
- }
-}
-
-registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts
deleted file mode 100644
index 9e9ba52..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-Copyright 2021 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 { DeviceInfo } from "./deviceinfo";
-import { IKeyBackupInfo } from "./keybackup";
-import { PassphraseInfo } from "../secret-storage";
-
-/* re-exports for backwards compatibility. */
-export {
- PassphraseInfo as IPassphraseInfo,
- SecretStorageKeyDescription as ISecretStorageKeyInfo,
-} from "../secret-storage";
-
-// TODO: Merge this with crypto.js once converted
-
-export enum CrossSigningKey {
- Master = "master",
- SelfSigning = "self_signing",
- UserSigning = "user_signing",
-}
-
-export interface IEncryptedEventInfo {
- /**
- * whether the event is encrypted (if not encrypted, some of the other properties may not be set)
- */
- encrypted: boolean;
-
- /**
- * the sender's key
- */
- senderKey: string;
-
- /**
- * the algorithm used to encrypt the event
- */
- algorithm: string;
-
- /**
- * whether we can be sure that the owner of the senderKey sent the event
- */
- authenticated: boolean;
-
- /**
- * the sender's device information, if available
- */
- sender?: DeviceInfo;
-
- /**
- * if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
- */
- mismatchedSender: boolean;
-}
-
-export interface IRecoveryKey {
- keyInfo?: IAddSecretStorageKeyOpts;
- privateKey: Uint8Array;
- encodedPrivateKey?: string;
-}
-
-export interface ICreateSecretStorageOpts {
- /**
- * Function called to await a secret storage key creation flow.
- * @returns Promise resolving to an object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- */
- createSecretStorageKey?: () => Promise<IRecoveryKey>;
-
- /**
- * The current key backup object. If passed,
- * the passphrase and recovery key from this backup will be used.
- */
- keyBackupInfo?: IKeyBackupInfo;
-
- /**
- * If true, a new key backup version will be
- * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
- * is supplied.
- */
- setupNewKeyBackup?: boolean;
-
- /**
- * Reset even if keys already exist.
- */
- setupNewSecretStorage?: boolean;
-
- /**
- * Function called to get the user's
- * current key backup passphrase. Should return a promise that resolves with a Uint8Array
- * containing the key, or rejects if the key cannot be obtained.
- */
- getKeyBackupPassphrase?: () => Promise<Uint8Array>;
-}
-
-export interface IAddSecretStorageKeyOpts {
- pubkey?: string;
- passphrase?: PassphraseInfo;
- name?: string;
- key?: Uint8Array;
-}
-
-export interface IImportOpts {
- stage: string; // TODO: Enum
- successes: number;
- failures: number;
- total: number;
-}
-
-export interface IImportRoomKeysOpts {
- /** called with an object that has a "stage" param */
- progressCallback?: (stage: IImportOpts) => void;
- untrusted?: boolean;
- source?: string; // TODO: Enum
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts
deleted file mode 100644
index d240bda..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts
+++ /dev/null
@@ -1,813 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-/**
- * Classes for dealing with key backup.
- */
-
-import type { IMegolmSessionData } from "../@types/crypto";
-import { MatrixClient } from "../client";
-import { logger } from "../logger";
-import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
-import { DeviceInfo } from "./deviceinfo";
-import { DeviceTrustLevel } from "./CrossSigning";
-import { keyFromPassphrase } from "./key_passphrase";
-import { safeSet, sleep } from "../utils";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { encodeRecoveryKey } from "./recoverykey";
-import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
-import {
- Curve25519SessionData,
- IAes256AuthData,
- ICurve25519AuthData,
- IKeyBackupInfo,
- IKeyBackupSession,
-} from "./keybackup";
-import { UnstableValue } from "../NamespacedValue";
-import { CryptoEvent } from "./index";
-import { crypto } from "./crypto";
-import { HTTPError, MatrixError } from "../http-api";
-
-const KEY_BACKUP_KEYS_PER_REQUEST = 200;
-const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
-
-type AuthData = IKeyBackupInfo["auth_data"];
-
-type SigInfo = {
- deviceId: string;
- valid?: boolean | null; // true: valid, false: invalid, null: cannot attempt validation
- device?: DeviceInfo | null;
- crossSigningId?: boolean;
- deviceTrust?: DeviceTrustLevel;
-};
-
-export type TrustInfo = {
- usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
- sigs: SigInfo[];
- // eslint-disable-next-line camelcase
- trusted_locally?: boolean;
-};
-
-export interface IKeyBackupCheck {
- backupInfo?: IKeyBackupInfo;
- trustInfo: TrustInfo;
-}
-
-/* eslint-disable camelcase */
-export interface IPreparedKeyBackupVersion {
- algorithm: string;
- auth_data: AuthData;
- recovery_key: string;
- privateKey: Uint8Array;
-}
-/* eslint-enable camelcase */
-
-/** A function used to get the secret key for a backup.
- */
-type GetKey = () => Promise<ArrayLike<number>>;
-
-interface BackupAlgorithmClass {
- algorithmName: string;
- // initialize from an existing backup
- init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
-
- // prepare a brand new backup
- prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]>;
-
- checkBackupVersion(info: IKeyBackupInfo): void;
-}
-
-interface BackupAlgorithm {
- untrusted: boolean;
- encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
- decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
- authData: AuthData;
- keyMatches(key: ArrayLike<number>): Promise<boolean>;
- free(): void;
-}
-
-export interface IKeyBackup {
- rooms: {
- [roomId: string]: {
- sessions: {
- [sessionId: string]: IKeyBackupSession;
- };
- };
- };
-}
-
-/**
- * Manages the key backup.
- */
-export class BackupManager {
- private algorithm: BackupAlgorithm | undefined;
- public backupInfo: IKeyBackupInfo | undefined; // The info dict from /room_keys/version
- public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
- private sendingBackups: boolean; // Are we currently sending backups?
- private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
-
- public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
- this.checkedForBackup = false;
- this.sendingBackups = false;
- }
-
- public get version(): string | undefined {
- return this.backupInfo && this.backupInfo.version;
- }
-
- /**
- * Performs a quick check to ensure that the backup info looks sane.
- *
- * Throws an error if a problem is detected.
- *
- * @param info - the key backup info
- */
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- const Algorithm = algorithmsByName[info.algorithm];
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm: " + info.algorithm);
- }
- if (typeof info.auth_data !== "object") {
- throw new Error("Invalid backup data returned");
- }
- return Algorithm.checkBackupVersion(info);
- }
-
- public static makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
- const Algorithm = algorithmsByName[info.algorithm];
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm");
- }
- return Algorithm.init(info.auth_data, getKey);
- }
-
- public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
- this.backupInfo = info;
- if (this.algorithm) {
- this.algorithm.free();
- }
-
- this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
-
- this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
-
- // There may be keys left over from a partially completed backup, so
- // schedule a send to check.
- this.scheduleKeyBackupSend();
- }
-
- /**
- * Disable backing up of keys.
- */
- public disableKeyBackup(): void {
- if (this.algorithm) {
- this.algorithm.free();
- }
- this.algorithm = undefined;
-
- this.backupInfo = undefined;
-
- this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
- }
-
- public getKeyBackupEnabled(): boolean | null {
- if (!this.checkedForBackup) {
- return null;
- }
- return Boolean(this.algorithm);
- }
-
- public async prepareKeyBackupVersion(
- key?: string | Uint8Array | null,
- algorithm?: string | undefined,
- ): Promise<IPreparedKeyBackupVersion> {
- const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm");
- }
-
- const [privateKey, authData] = await Algorithm.prepare(key);
- const recoveryKey = encodeRecoveryKey(privateKey)!;
- return {
- algorithm: Algorithm.algorithmName,
- auth_data: authData,
- recovery_key: recoveryKey,
- privateKey,
- };
- }
-
- public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<void> {
- this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
- }
-
- /**
- * Check the server for an active key backup and
- * if one is present and has a valid signature from
- * one of the user's verified devices, start backing up
- * to it.
- */
- public async checkAndStart(): Promise<IKeyBackupCheck | null> {
- logger.log("Checking key backup status...");
- if (this.baseApis.isGuest()) {
- logger.log("Skipping key backup check since user is guest");
- this.checkedForBackup = true;
- return null;
- }
- let backupInfo: IKeyBackupInfo | undefined;
- try {
- backupInfo = (await this.baseApis.getKeyBackupVersion()) ?? undefined;
- } catch (e) {
- logger.log("Error checking for active key backup", e);
- if ((<HTTPError>e).httpStatus === 404) {
- // 404 is returned when the key backup does not exist, so that
- // counts as successfully checking.
- this.checkedForBackup = true;
- }
- return null;
- }
- this.checkedForBackup = true;
-
- const trustInfo = await this.isKeyBackupTrusted(backupInfo);
-
- if (trustInfo.usable && !this.backupInfo) {
- logger.log(`Found usable key backup v${backupInfo!.version}: enabling key backups`);
- await this.enableKeyBackup(backupInfo!);
- } else if (!trustInfo.usable && this.backupInfo) {
- logger.log("No usable key backup: disabling key backup");
- this.disableKeyBackup();
- } else if (!trustInfo.usable && !this.backupInfo) {
- logger.log("No usable key backup: not enabling key backup");
- } else if (trustInfo.usable && this.backupInfo) {
- // may not be the same version: if not, we should switch
- if (backupInfo!.version !== this.backupInfo.version) {
- logger.log(
- `On backup version ${this.backupInfo.version} but ` +
- `found version ${backupInfo!.version}: switching.`,
- );
- this.disableKeyBackup();
- await this.enableKeyBackup(backupInfo!);
- // We're now using a new backup, so schedule all the keys we have to be
- // uploaded to the new backup. This is a bit of a workaround to upload
- // keys to a new backup in *most* cases, but it won't cover all cases
- // because we don't remember what backup version we uploaded keys to:
- // see https://github.com/vector-im/element-web/issues/14833
- await this.scheduleAllGroupSessionsForBackup();
- } else {
- logger.log(`Backup version ${backupInfo!.version} still current`);
- }
- }
-
- return { backupInfo, trustInfo };
- }
-
- /**
- * Forces a re-check of the key backup and enables/disables it
- * as appropriate.
- *
- * @returns Object with backup info (as returned by
- * getKeyBackupVersion) in backupInfo and
- * trust information (as returned by isKeyBackupTrusted)
- * in trustInfo.
- */
- public async checkKeyBackup(): Promise<IKeyBackupCheck | null> {
- this.checkedForBackup = false;
- return this.checkAndStart();
- }
-
- /**
- * Attempts to retrieve a session from a key backup, if enough time
- * has elapsed since the last check for this session id.
- */
- public async queryKeyBackupRateLimited(
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- ): Promise<void> {
- if (!this.backupInfo) {
- return;
- }
-
- const now = new Date().getTime();
- if (
- !this.sessionLastCheckAttemptedTime[targetSessionId!] ||
- now - this.sessionLastCheckAttemptedTime[targetSessionId!] > KEY_BACKUP_CHECK_RATE_LIMIT
- ) {
- this.sessionLastCheckAttemptedTime[targetSessionId!] = now;
- await this.baseApis.restoreKeyBackupWithCache(targetRoomId!, targetSessionId!, this.backupInfo, {});
- }
- }
-
- /**
- * Check if the given backup info is trusted.
- *
- * @param backupInfo - key backup info dict from /room_keys/version
- */
- public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise<TrustInfo> {
- const ret = {
- usable: false,
- trusted_locally: false,
- sigs: [] as SigInfo[],
- };
-
- if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) {
- logger.info("Key backup is absent or missing required data");
- return ret;
- }
-
- const userId = this.baseApis.getUserId()!;
- const privKey = await this.baseApis.crypto!.getSessionBackupPrivateKey();
- if (privKey) {
- let algorithm: BackupAlgorithm | null = null;
- try {
- algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
-
- if (await algorithm.keyMatches(privKey)) {
- logger.info("Backup is trusted locally");
- ret.trusted_locally = true;
- }
- } catch {
- // do nothing -- if we have an error, then we don't mark it as
- // locally trusted
- } finally {
- algorithm?.free();
- }
- }
-
- const mySigs = backupInfo.auth_data.signatures[userId] || {};
-
- for (const keyId of Object.keys(mySigs)) {
- const keyIdParts = keyId.split(":");
- if (keyIdParts[0] !== "ed25519") {
- logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
- continue;
- }
- // Could be a cross-signing master key, but just say this is the device
- // ID for backwards compat
- const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
-
- // first check to see if it's from our cross-signing key
- const crossSigningId = this.baseApis.crypto!.crossSigningInfo.getId();
- if (crossSigningId === sigInfo.deviceId) {
- sigInfo.crossSigningId = true;
- try {
- await verifySignature(
- this.baseApis.crypto!.olmDevice,
- backupInfo.auth_data,
- userId,
- sigInfo.deviceId,
- crossSigningId,
- );
- sigInfo.valid = true;
- } catch (e) {
- logger.warn("Bad signature from cross signing key " + crossSigningId, e);
- sigInfo.valid = false;
- }
- ret.sigs.push(sigInfo);
- continue;
- }
-
- // Now look for a sig from a device
- // At some point this can probably go away and we'll just support
- // it being signed by the cross-signing master key
- const device = this.baseApis.crypto!.deviceList.getStoredDevice(userId, sigInfo.deviceId);
- if (device) {
- sigInfo.device = device;
- sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId);
- try {
- await verifySignature(
- this.baseApis.crypto!.olmDevice,
- backupInfo.auth_data,
- userId,
- device.deviceId,
- device.getFingerprint(),
- );
- sigInfo.valid = true;
- } catch (e) {
- logger.info(
- "Bad signature from key ID " +
- keyId +
- " userID " +
- this.baseApis.getUserId() +
- " device ID " +
- device.deviceId +
- " fingerprint: " +
- device.getFingerprint(),
- backupInfo.auth_data,
- e,
- );
- sigInfo.valid = false;
- }
- } else {
- sigInfo.valid = null; // Can't determine validity because we don't have the signing device
- logger.info("Ignoring signature from unknown key " + keyId);
- }
- ret.sigs.push(sigInfo);
- }
-
- ret.usable = ret.sigs.some((s) => {
- return s.valid && ((s.device && s.deviceTrust?.isVerified()) || s.crossSigningId);
- });
- return ret;
- }
-
- /**
- * Schedules sending all keys waiting to be sent to the backup, if not already
- * scheduled. Retries if necessary.
- *
- * @param maxDelay - Maximum delay to wait in ms. 0 means no delay.
- */
- public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
- if (this.sendingBackups) return;
-
- this.sendingBackups = true;
-
- try {
- // wait between 0 and `maxDelay` seconds, to avoid backup
- // requests from different clients hitting the server all at
- // the same time when a new key is sent
- const delay = Math.random() * maxDelay;
- await sleep(delay);
- let numFailures = 0; // number of consecutive failures
- for (;;) {
- if (!this.algorithm) {
- return;
- }
- try {
- const numBackedUp = await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
- if (numBackedUp === 0) {
- // no sessions left needing backup: we're done
- return;
- }
- numFailures = 0;
- } catch (err) {
- numFailures++;
- logger.log("Key backup request failed", err);
- if ((<MatrixError>err).data) {
- if (
- (<MatrixError>err).data.errcode == "M_NOT_FOUND" ||
- (<MatrixError>err).data.errcode == "M_WRONG_ROOM_KEYS_VERSION"
- ) {
- // Re-check key backup status on error, so we can be
- // sure to present the current situation when asked.
- await this.checkKeyBackup();
- // Backup version has changed or this backup version
- // has been deleted
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (<MatrixError>err).data.errcode!);
- throw err;
- }
- }
- }
- if (numFailures) {
- // exponential backoff if we have failures
- await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
- }
- }
- } finally {
- this.sendingBackups = false;
- }
- }
-
- /**
- * Take some e2e keys waiting to be backed up and send them
- * to the backup.
- *
- * @param limit - Maximum number of keys to back up
- * @returns Number of sessions backed up
- */
- public async backupPendingKeys(limit: number): Promise<number> {
- const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit);
- if (!sessions.length) {
- return 0;
- }
-
- let remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
-
- const rooms: IKeyBackup["rooms"] = {};
- for (const session of sessions) {
- const roomId = session.sessionData!.room_id;
- safeSet(rooms, roomId, rooms[roomId] || { sessions: {} });
-
- const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
- session.senderKey,
- session.sessionId,
- session.sessionData!,
- );
- sessionData.algorithm = MEGOLM_ALGORITHM;
-
- const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length;
-
- const userId = this.baseApis.crypto!.deviceList.getUserByIdentityKey(MEGOLM_ALGORITHM, session.senderKey);
- const device =
- this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(MEGOLM_ALGORITHM, session.senderKey) ??
- undefined;
- const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
-
- safeSet(rooms[roomId]["sessions"], session.sessionId, {
- first_message_index: sessionData.first_known_index,
- forwarded_count: forwardedCount,
- is_verified: verified,
- session_data: await this.algorithm!.encryptSession(sessionData),
- });
- }
-
- await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
-
- await this.baseApis.crypto!.cryptoStore.unmarkSessionsNeedingBackup(sessions);
- remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
-
- return sessions.length;
- }
-
- public async backupGroupSession(senderKey: string, sessionId: string): Promise<void> {
- await this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([
- {
- senderKey: senderKey,
- sessionId: sessionId,
- },
- ]);
-
- if (this.backupInfo) {
- // don't wait for this to complete: it will delay so
- // happens in the background
- this.scheduleKeyBackupSend();
- }
- // if this.backupInfo is not set, then the keys will be backed up when
- // this.enableKeyBackup is called
- }
-
- /**
- * Marks all group sessions as needing to be backed up and schedules them to
- * upload in the background as soon as possible.
- */
- public async scheduleAllGroupSessionsForBackup(): Promise<void> {
- await this.flagAllGroupSessionsForBackup();
-
- // Schedule keys to upload in the background as soon as possible.
- this.scheduleKeyBackupSend(0 /* maxDelay */);
- }
-
- /**
- * Marks all group sessions as needing to be backed up without scheduling
- * them to upload in the background.
- * @returns Promise which resolves to the number of sessions now requiring a backup
- * (which will be equal to the number of sessions in the store).
- */
- public async flagAllGroupSessionsForBackup(): Promise<number> {
- await this.baseApis.crypto!.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP],
- (txn) => {
- this.baseApis.crypto!.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
- if (session !== null) {
- this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([session], txn);
- }
- });
- },
- );
-
- const remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
- return remaining;
- }
-
- /**
- * Counts the number of end to end session keys that are waiting to be backed up
- * @returns Promise which resolves to the number of sessions requiring backup
- */
- public countSessionsNeedingBackup(): Promise<number> {
- return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- }
-}
-
-export class Curve25519 implements BackupAlgorithm {
- public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
-
- public constructor(
- public authData: ICurve25519AuthData,
- private publicKey: any, // FIXME: PkEncryption
- private getKey: () => Promise<Uint8Array>,
- ) {}
-
- public static async init(authData: AuthData, getKey: () => Promise<Uint8Array>): Promise<Curve25519> {
- if (!authData || !("public_key" in authData)) {
- throw new Error("auth_data missing required information");
- }
- const publicKey = new global.Olm.PkEncryption();
- publicKey.set_recipient_key(authData.public_key);
- return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
- }
-
- public static async prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]> {
- const decryption = new global.Olm.PkDecryption();
- try {
- const authData: Partial<ICurve25519AuthData> = {};
- if (!key) {
- authData.public_key = decryption.generate_key();
- } else if (key instanceof Uint8Array) {
- authData.public_key = decryption.init_with_private_key(key);
- } else {
- const derivation = await keyFromPassphrase(key);
- authData.private_key_salt = derivation.salt;
- authData.private_key_iterations = derivation.iterations;
- authData.public_key = decryption.init_with_private_key(derivation.key);
- }
- const publicKey = new global.Olm.PkEncryption();
- publicKey.set_recipient_key(authData.public_key);
-
- return [decryption.get_private_key(), authData as AuthData];
- } finally {
- decryption.free();
- }
- }
-
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- if (!("public_key" in info.auth_data)) {
- throw new Error("Invalid backup data returned");
- }
- }
-
- public get untrusted(): boolean {
- return true;
- }
-
- public async encryptSession(data: Record<string, any>): Promise<Curve25519SessionData> {
- const plainText: Record<string, any> = Object.assign({}, data);
- delete plainText.session_id;
- delete plainText.room_id;
- delete plainText.first_known_index;
- return this.publicKey.encrypt(JSON.stringify(plainText));
- }
-
- public async decryptSessions(
- sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
- ): Promise<IMegolmSessionData[]> {
- const privKey = await this.getKey();
- const decryption = new global.Olm.PkDecryption();
- try {
- const backupPubKey = decryption.init_with_private_key(privKey);
-
- if (backupPubKey !== this.authData.public_key) {
- throw new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
- }
-
- const keys: IMegolmSessionData[] = [];
-
- for (const [sessionId, sessionData] of Object.entries(sessions)) {
- try {
- const decrypted = JSON.parse(
- decryption.decrypt(
- sessionData.session_data.ephemeral,
- sessionData.session_data.mac,
- sessionData.session_data.ciphertext,
- ),
- );
- decrypted.session_id = sessionId;
- keys.push(decrypted);
- } catch (e) {
- logger.log("Failed to decrypt megolm session from backup", e, sessionData);
- }
- }
- return keys;
- } finally {
- decryption.free();
- }
- }
-
- public async keyMatches(key: Uint8Array): Promise<boolean> {
- const decryption = new global.Olm.PkDecryption();
- let pubKey: string;
- try {
- pubKey = decryption.init_with_private_key(key);
- } finally {
- decryption.free();
- }
-
- return pubKey === this.authData.public_key;
- }
-
- public free(): void {
- this.publicKey.free();
- }
-}
-
-function randomBytes(size: number): Uint8Array {
- const buf = new Uint8Array(size);
- crypto.getRandomValues(buf);
- return buf;
-}
-
-const UNSTABLE_MSC3270_NAME = new UnstableValue(
- "m.megolm_backup.v1.aes-hmac-sha2",
- "org.matrix.msc3270.v1.aes-hmac-sha2",
-);
-
-export class Aes256 implements BackupAlgorithm {
- public static algorithmName = UNSTABLE_MSC3270_NAME.name;
-
- public constructor(public readonly authData: IAes256AuthData, private readonly key: Uint8Array) {}
-
- public static async init(authData: IAes256AuthData, getKey: () => Promise<Uint8Array>): Promise<Aes256> {
- if (!authData) {
- throw new Error("auth_data missing");
- }
- const key = await getKey();
- if (authData.mac) {
- const { mac } = await calculateKeyCheck(key, authData.iv);
- if (authData.mac.replace(/=+$/g, "") !== mac.replace(/=+/g, "")) {
- throw new Error("Key does not match");
- }
- }
- return new Aes256(authData, key);
- }
-
- public static async prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]> {
- let outKey: Uint8Array;
- const authData: Partial<IAes256AuthData> = {};
- if (!key) {
- outKey = randomBytes(32);
- } else if (key instanceof Uint8Array) {
- outKey = new Uint8Array(key);
- } else {
- const derivation = await keyFromPassphrase(key);
- authData.private_key_salt = derivation.salt;
- authData.private_key_iterations = derivation.iterations;
- outKey = derivation.key;
- }
-
- const { iv, mac } = await calculateKeyCheck(outKey);
- authData.iv = iv;
- authData.mac = mac;
-
- return [outKey, authData as AuthData];
- }
-
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
- throw new Error("Invalid backup data returned");
- }
- }
-
- public get untrusted(): boolean {
- return false;
- }
-
- public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
- 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);
- }
-
- public async decryptSessions(
- sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
- ): 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));
- decrypted.session_id = sessionId;
- keys.push(decrypted);
- } catch (e) {
- logger.log("Failed to decrypt megolm session from backup", e, sessionData);
- }
- }
- return keys;
- }
-
- public async keyMatches(key: Uint8Array): Promise<boolean> {
- if (this.authData.mac) {
- const { mac } = await calculateKeyCheck(key, this.authData.iv);
- return this.authData.mac.replace(/=+$/g, "") === mac.replace(/=+/g, "");
- } else {
- // if we have no information, we have to assume the key is right
- return true;
- }
- }
-
- public free(): void {
- this.key.fill(0);
- }
-}
-
-export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
- [Curve25519.algorithmName]: Curve25519,
- [Aes256.algorithmName]: Aes256,
-};
-
-export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts
deleted file mode 100644
index 704754f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../logger";
-
-export let crypto = global.window?.crypto;
-export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
-export let TextEncoder = global.window?.TextEncoder;
-
-/* eslint-disable @typescript-eslint/no-var-requires */
-if (!crypto) {
- try {
- crypto = require("crypto").webcrypto;
- } catch (e) {
- logger.error("Failed to load webcrypto", e);
- }
-}
-if (!subtleCrypto) {
- subtleCrypto = crypto?.subtle;
-}
-if (!TextEncoder) {
- try {
- TextEncoder = require("util").TextEncoder;
- } catch (e) {
- logger.error("Failed to load TextEncoder util", e);
- }
-}
-/* eslint-enable @typescript-eslint/no-var-requires */
-
-export function setCrypto(_crypto: Crypto): void {
- crypto = _crypto;
- subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle;
-}
-
-export function setTextEncoder(_TextEncoder: typeof TextEncoder): void {
- TextEncoder = _TextEncoder;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
deleted file mode 100644
index 373b236..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
-Copyright 2020-2021 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 anotherjson from "another-json";
-
-import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
-import { decodeBase64, encodeBase64 } from "./olmlib";
-import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
-import { decryptAES, encryptAES } from "./aes";
-import { logger } from "../logger";
-import { Crypto } from "./index";
-import { Method } from "../http-api";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-export interface IDehydratedDevice {
- device_id: string; // eslint-disable-line camelcase
- device_data: SecretStorageKeyDescription & {
- // eslint-disable-line camelcase
- algorithm: string;
- account: string; // pickle
- };
-}
-
-export interface IDehydratedDeviceKeyInfo {
- passphrase?: string;
-}
-
-export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
-
-const oneweek = 7 * 24 * 60 * 60 * 1000;
-
-export class DehydrationManager {
- private inProgress = false;
- private timeoutId: any;
- private key?: Uint8Array;
- private keyInfo?: { [props: string]: any };
- private deviceDisplayName?: string;
-
- public constructor(private readonly crypto: Crypto) {
- this.getDehydrationKeyFromCache();
- }
-
- public getDehydrationKeyFromCache(): Promise<void> {
- return this.crypto.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.getSecretStorePrivateKey(
- txn,
- async (result) => {
- if (result) {
- const { key, keyInfo, deviceDisplayName, time } = result;
- const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
- this.key = decodeBase64(decrypted);
- this.keyInfo = keyInfo;
- this.deviceDisplayName = deviceDisplayName;
- const now = Date.now();
- const delay = Math.max(1, time + oneweek - now);
- this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), delay);
- }
- },
- "dehydration",
- );
- });
- }
-
- /** set the key, and queue periodic dehydration to the server in the background */
- public async setKeyAndQueueDehydration(
- key: Uint8Array,
- keyInfo: { [props: string]: any } = {},
- deviceDisplayName?: string,
- ): Promise<void> {
- const matches = await this.setKey(key, keyInfo, deviceDisplayName);
- if (!matches) {
- // start dehydration in the background
- this.dehydrateDevice();
- }
- }
-
- public async setKey(
- key: Uint8Array,
- keyInfo: { [props: string]: any } = {},
- deviceDisplayName?: string,
- ): Promise<boolean | undefined> {
- if (!key) {
- // unsetting the key -- cancel any pending dehydration task
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- // clear storage
- await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", null);
- });
- this.key = undefined;
- this.keyInfo = undefined;
- return;
- }
-
- // Check to see if it's the same key as before. If it's different,
- // dehydrate a new device. If it's the same, we can keep the same
- // device. (Assume that keyInfo and deviceDisplayName will be the
- // same if the key is the same.)
- let matches: boolean = !!this.key && key.length == this.key.length;
- for (let i = 0; matches && i < key.length; i++) {
- if (key[i] != this.key![i]) {
- matches = false;
- }
- }
- if (!matches) {
- this.key = key;
- this.keyInfo = keyInfo;
- this.deviceDisplayName = deviceDisplayName;
- }
- return matches;
- }
-
- /** returns the device id of the newly created dehydrated device */
- public async dehydrateDevice(): Promise<string | undefined> {
- if (this.inProgress) {
- logger.log("Dehydration already in progress -- not starting new dehydration");
- return;
- }
- this.inProgress = true;
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- try {
- 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);
- await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
- keyInfo: this.keyInfo,
- key,
- deviceDisplayName: this.deviceDisplayName!,
- time: Date.now(),
- });
- });
- logger.log("Attempting to dehydrate device");
-
- logger.log("Creating account");
- // create the account and all the necessary keys
- const account = new global.Olm.Account();
- account.create();
- const e2eKeys = JSON.parse(account.identity_keys());
-
- const maxKeys = account.max_number_of_one_time_keys();
- // FIXME: generate in small batches?
- account.generate_one_time_keys(maxKeys / 2);
- account.generate_fallback_key();
- const otks: Record<string, string> = JSON.parse(account.one_time_keys());
- const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
- account.mark_keys_as_published();
-
- // dehydrate the account and store it on the server
- const pickledAccount = account.pickle(new Uint8Array(this.key!));
-
- const deviceData: { [props: string]: any } = {
- algorithm: DEHYDRATION_ALGORITHM,
- account: pickledAccount,
- };
- if (this.keyInfo!.passphrase) {
- deviceData.passphrase = this.keyInfo!.passphrase;
- }
-
- logger.log("Uploading account to server");
- // eslint-disable-next-line camelcase
- const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
- Method.Put,
- "/dehydrated_device",
- undefined,
- {
- device_data: deviceData,
- initial_device_display_name: this.deviceDisplayName,
- },
- {
- prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
- },
- );
-
- // send the keys to the server
- const deviceId = dehydrateResult.device_id;
- logger.log("Preparing device keys", deviceId);
- const deviceKeys: IDeviceKeys = {
- algorithms: this.crypto.supportedAlgorithms,
- device_id: deviceId,
- user_id: this.crypto.userId,
- keys: {
- [`ed25519:${deviceId}`]: e2eKeys.ed25519,
- [`curve25519:${deviceId}`]: e2eKeys.curve25519,
- },
- };
- const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
- deviceKeys.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: deviceSignature,
- },
- };
- if (this.crypto.crossSigningInfo.getId("self_signing")) {
- await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
- }
-
- logger.log("Preparing one-time keys");
- const oneTimeKeys: Record<string, IOneTimeKey> = {};
- for (const [keyId, key] of Object.entries(otks.curve25519)) {
- const k: IOneTimeKey = { key };
- const signature = account.sign(anotherjson.stringify(k));
- k.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: signature,
- },
- };
- oneTimeKeys[`signed_curve25519:${keyId}`] = k;
- }
-
- logger.log("Preparing fallback keys");
- const fallbackKeys: Record<string, IOneTimeKey> = {};
- for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
- const k: IOneTimeKey = { key, fallback: true };
- const signature = account.sign(anotherjson.stringify(k));
- k.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: signature,
- },
- };
- fallbackKeys[`signed_curve25519:${keyId}`] = k;
- }
-
- logger.log("Uploading keys to server");
- await this.crypto.baseApis.http.authedRequest(
- Method.Post,
- "/keys/upload/" + encodeURI(deviceId),
- undefined,
- {
- "device_keys": deviceKeys,
- "one_time_keys": oneTimeKeys,
- "org.matrix.msc2732.fallback_keys": fallbackKeys,
- },
- );
- logger.log("Done dehydrating");
-
- // dehydrate again in a week
- this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek);
-
- return deviceId;
- } finally {
- this.inProgress = false;
- }
- }
-
- public stop(): void {
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts
deleted file mode 100644
index b4bb4fd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { ISignatures } from "../@types/signed";
-
-export interface IDevice {
- keys: Record<string, string>;
- algorithms: string[];
- verified: DeviceVerification;
- known: boolean;
- unsigned?: Record<string, any>;
- signatures?: ISignatures;
-}
-
-enum DeviceVerification {
- Blocked = -1,
- Unverified = 0,
- Verified = 1,
-}
-
-/**
- * Information about a user's device
- */
-export class DeviceInfo {
- /**
- * rehydrate a DeviceInfo from the session store
- *
- * @param obj - raw object from session store
- * @param deviceId - id of the device
- *
- * @returns new DeviceInfo
- */
- public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
- const res = new DeviceInfo(deviceId);
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- // @ts-ignore - this is messy and typescript doesn't like it
- res[prop as keyof IDevice] = obj[prop as keyof IDevice];
- }
- }
- return res;
- }
-
- public static DeviceVerification = {
- VERIFIED: DeviceVerification.Verified,
- UNVERIFIED: DeviceVerification.Unverified,
- BLOCKED: DeviceVerification.Blocked,
- };
-
- /** list of algorithms supported by this device */
- public algorithms: string[] = [];
- /** a map from `<key type>:<id> -> <base64-encoded key>` */
- public keys: Record<string, string> = {};
- /** whether the device has been verified/blocked by the user */
- public verified = DeviceVerification.Unverified;
- /**
- * whether the user knows of this device's existence
- * (useful when warning the user that a user has added new devices)
- */
- public known = false;
- /** additional data from the homeserver */
- public unsigned: Record<string, any> = {};
- public signatures: ISignatures = {};
-
- /**
- * @param deviceId - id of the device
- */
- public constructor(public readonly deviceId: string) {}
-
- /**
- * Prepare a DeviceInfo for JSON serialisation in the session store
- *
- * @returns deviceinfo with non-serialised members removed
- */
- public toStorage(): IDevice {
- return {
- algorithms: this.algorithms,
- keys: this.keys,
- verified: this.verified,
- known: this.known,
- unsigned: this.unsigned,
- signatures: this.signatures,
- };
- }
-
- /**
- * Get the fingerprint for this device (ie, the Ed25519 key)
- *
- * @returns base64-encoded fingerprint of this device
- */
- public getFingerprint(): string {
- return this.keys["ed25519:" + this.deviceId];
- }
-
- /**
- * Get the identity key for this device (ie, the Curve25519 key)
- *
- * @returns base64-encoded identity key of this device
- */
- public getIdentityKey(): string {
- return this.keys["curve25519:" + this.deviceId];
- }
-
- /**
- * Get the configured display name for this device, if any
- *
- * @returns displayname
- */
- public getDisplayName(): string | null {
- return this.unsigned.device_display_name || null;
- }
-
- /**
- * Returns true if this device is blocked
- *
- * @returns true if blocked
- */
- public isBlocked(): boolean {
- return this.verified == DeviceVerification.Blocked;
- }
-
- /**
- * Returns true if this device is verified
- *
- * @returns true if verified
- */
- public isVerified(): boolean {
- return this.verified == DeviceVerification.Verified;
- }
-
- /**
- * Returns true if this device is unverified
- *
- * @returns true if unverified
- */
- public isUnverified(): boolean {
- return this.verified == DeviceVerification.Unverified;
- }
-
- /**
- * Returns true if the user knows about this device's existence
- *
- * @returns true if known
- */
- public isKnown(): boolean {
- return this.known === true;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts
deleted file mode 100644
index 68df6ca..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts
+++ /dev/null
@@ -1,3936 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2018-2019 New Vector Ltd
-Copyright 2019-2021 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 anotherjson from "another-json";
-import { v4 as uuidv4 } from "uuid";
-
-import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
-import type { PkDecryption, PkSigning } from "@matrix-org/olm";
-import { EventType, ToDeviceMessageId } from "../@types/event";
-import { TypedReEmitter } from "../ReEmitter";
-import { logger } from "../logger";
-import { IExportedDevice, OlmDevice } from "./OlmDevice";
-import { IOlmDevice } from "./algorithms/megolm";
-import * as olmlib from "./olmlib";
-import { DeviceInfoMap, DeviceList } from "./DeviceList";
-import { DeviceInfo, IDevice } from "./deviceinfo";
-import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
-import * as algorithms from "./algorithms";
-import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning";
-import { EncryptionSetupBuilder } from "./EncryptionSetup";
-import {
- IAccountDataClient,
- ISecretRequest,
- SECRET_STORAGE_ALGORITHM_V1_AES,
- SecretStorage,
- SecretStorageKeyObject,
- SecretStorageKeyTuple,
-} from "./SecretStorage";
-import {
- IAddSecretStorageKeyOpts,
- ICreateSecretStorageOpts,
- IEncryptedEventInfo,
- IImportRoomKeysOpts,
- IRecoveryKey,
-} from "./api";
-import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { VerificationBase } from "./verification/Base";
-import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from "./verification/QRCode";
-import { SAS as SASVerification } from "./verification/SAS";
-import { keyFromPassphrase } from "./key_passphrase";
-import { decodeRecoveryKey, encodeRecoveryKey } from "./recoverykey";
-import { VerificationRequest } from "./verification/request/VerificationRequest";
-import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel";
-import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/request/ToDeviceChannel";
-import { IllegalMethod } from "./verification/IllegalMethod";
-import { KeySignatureUploadError } from "../errors";
-import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
-import { DehydrationManager } from "./dehydration";
-import { BackupManager } from "./backup";
-import { IStore } from "../store";
-import { Room, RoomEvent } from "../models/room";
-import { RoomMember, RoomMemberEvent } from "../models/room-member";
-import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
-import { ToDeviceBatch } from "../models/ToDeviceMessage";
-import {
- ClientEvent,
- ICrossSigningKey,
- IKeysUploadResponse,
- ISignedKey,
- IUploadKeySignaturesResponse,
- MatrixClient,
-} from "../client";
-import type { IRoomEncryption, RoomList } from "./RoomList";
-import { IKeyBackupInfo } from "./keybackup";
-import { ISyncStateData } from "../sync";
-import { CryptoStore } from "./store/base";
-import { IVerificationChannel } from "./verification/request/Channel";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { IContent } from "../models/event";
-import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
-import { ISignatures } from "../@types/signed";
-import { IMessage } from "./algorithms/olm";
-import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
-import { RoomState, RoomStateEvent } from "../models/room-state";
-import { MapWithDefault, recursiveMapToObject } from "../utils";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-const DeviceVerification = DeviceInfo.DeviceVerification;
-
-const defaultVerificationMethods = {
- [ReciprocateQRCode.NAME]: ReciprocateQRCode,
- [SASVerification.NAME]: SASVerification,
-
- // These two can't be used for actual verification, but we do
- // need to be able to define them here for the verification flows
- // to start.
- [SHOW_QR_CODE_METHOD]: IllegalMethod,
- [SCAN_QR_CODE_METHOD]: IllegalMethod,
-} as const;
-
-/**
- * verification method names
- */
-// legacy export identifier
-export const verificationMethods = {
- RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
- SAS: SASVerification.NAME,
-} as const;
-
-export type VerificationMethod = keyof typeof verificationMethods | string;
-
-export function isCryptoAvailable(): boolean {
- return Boolean(global.Olm);
-}
-
-const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
-
-interface IInitOpts {
- exportedOlmDevice?: IExportedDevice;
- pickleKey?: string;
-}
-
-export interface IBootstrapCrossSigningOpts {
- /** Optional. Reset even if keys already exist. */
- setupNewCrossSigning?: boolean;
- /**
- * A function that makes the request requiring auth. Receives the auth data as an object.
- * Can be called multiple times, first with an empty authDict, to obtain the flows.
- */
- authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>;
-}
-
-export interface ICryptoCallbacks {
- getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
- saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
- shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
- getSecretStorageKey?: (
- keys: { keys: Record<string, SecretStorageKeyDescription> },
- name: string,
- ) => Promise<[string, Uint8Array] | null>;
- cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
- onSecretRequested?: (
- userId: string,
- deviceId: string,
- requestId: string,
- secretName: string,
- deviceTrust: DeviceTrustLevel,
- ) => Promise<string | undefined>;
- getDehydrationKey?: (
- keyInfo: SecretStorageKeyDescription,
- checkFunc: (key: Uint8Array) => void,
- ) => Promise<Uint8Array>;
- getBackupKey?: () => Promise<Uint8Array>;
-}
-
-/* eslint-disable camelcase */
-interface IRoomKey {
- room_id: string;
- algorithm: string;
-}
-
-/**
- * The parameters of a room key request. The details of the request may
- * vary with the crypto algorithm, but the management and storage layers for
- * outgoing requests expect it to have 'room_id' and 'session_id' properties.
- */
-export interface IRoomKeyRequestBody extends IRoomKey {
- session_id: string;
- sender_key: string;
-}
-
-/* eslint-enable camelcase */
-
-interface IDeviceVerificationUpgrade {
- devices: DeviceInfo[];
- crossSigningInfo: CrossSigningInfo;
-}
-
-export interface ICheckOwnCrossSigningTrustOpts {
- allowPrivateKeyRequests?: boolean;
-}
-
-interface IUserOlmSession {
- deviceIdKey: string;
- sessions: {
- sessionId: string;
- hasReceivedMessage: boolean;
- }[];
-}
-
-export interface IRoomKeyRequestRecipient {
- userId: string;
- deviceId: string;
-}
-
-interface ISignableObject {
- signatures?: ISignatures;
- unsigned?: object;
-}
-
-export interface IRequestsMap {
- getRequest(event: MatrixEvent): VerificationRequest | undefined;
- getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined;
- setRequest(event: MatrixEvent, request: VerificationRequest): void;
- setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
-}
-
-/* eslint-disable camelcase */
-export interface IOlmEncryptedContent {
- algorithm: typeof olmlib.OLM_ALGORITHM;
- sender_key: string;
- ciphertext: Record<string, IMessage>;
- [ToDeviceMessageId]?: string;
-}
-
-export interface IMegolmEncryptedContent {
- algorithm: typeof olmlib.MEGOLM_ALGORITHM;
- sender_key: string;
- session_id: string;
- device_id: string;
- ciphertext: string;
- [ToDeviceMessageId]?: string;
-}
-/* eslint-enable camelcase */
-
-export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent;
-
-export enum CryptoEvent {
- DeviceVerificationChanged = "deviceVerificationChanged",
- UserTrustStatusChanged = "userTrustStatusChanged",
- UserCrossSigningUpdated = "userCrossSigningUpdated",
- RoomKeyRequest = "crypto.roomKeyRequest",
- RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
- KeyBackupStatus = "crypto.keyBackupStatus",
- KeyBackupFailed = "crypto.keyBackupFailed",
- KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
- KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
- VerificationRequest = "crypto.verification.request",
- Warning = "crypto.warning",
- WillUpdateDevices = "crypto.willUpdateDevices",
- DevicesUpdated = "crypto.devicesUpdated",
- KeysChanged = "crossSigning.keysChanged",
-}
-
-export type CryptoEventHandlerMap = {
- /**
- * Fires when a device is marked as verified/unverified/blocked/unblocked by
- * {@link MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or
- * {@link MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}.
- *
- * @param userId - the owner of the verified device
- * @param deviceId - the id of the verified device
- * @param deviceInfo - updated device information
- */
- [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
- /**
- * Fires when the trust status of a user changes
- * If userId is the userId of the logged-in user, this indicated a change
- * in the trust status of the cross-signing data on the account.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- * @experimental
- *
- * @param userId - the userId of the user in question
- * @param trustLevel - The new trust level of the user
- */
- [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
- /**
- * Fires when we receive a room key request
- *
- * @param req - request details
- */
- [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
- /**
- * Fires when we receive a room key request cancellation
- */
- [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
- /**
- * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
- * @param enabled - true if key backup has been enabled, otherwise false
- * @example
- * ```
- * matrixClient.on("crypto.keyBackupStatus", function(enabled){
- * if (enabled) {
- * [...]
- * }
- * });
- * ```
- */
- [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
- [CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
- [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
- [CryptoEvent.KeySignatureUploadFailure]: (
- failures: IUploadKeySignaturesResponse["failures"],
- source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
- upload: (opts: { shouldEmit: boolean }) => Promise<void>,
- ) => void;
- /**
- * Fires when a key verification is requested.
- */
- [CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
- /**
- * Fires when the app may wish to warn the user about something related
- * the end-to-end crypto.
- *
- * @param type - One of the strings listed above
- */
- [CryptoEvent.Warning]: (type: string) => void;
- /**
- * Fires when the user's cross-signing keys have changed or cross-signing
- * has been enabled/disabled. The client can use getStoredCrossSigningForUser
- * with the user ID of the logged in user to check if cross-signing is
- * enabled on the account. If enabled, it can test whether the current key
- * is trusted using with checkUserTrust with the user ID of the logged
- * in user. The checkOwnCrossSigningTrust function may be used to reconcile
- * the trust in the account key.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- * @experimental
- */
- [CryptoEvent.KeysChanged]: (data: {}) => void;
- /**
- * Fires whenever the stored devices for a user will be updated
- * @param users - A list of user IDs that will be updated
- * @param initialFetch - If true, the store is empty (apart
- * from our own device) and is being seeded.
- */
- [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
- /**
- * Fires whenever the stored devices for a user have changed
- * @param users - A list of user IDs that were updated
- * @param initialFetch - If true, the store was empty (apart
- * from our own device) and has been seeded.
- */
- [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
- [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
-};
-
-export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> implements CryptoBackend {
- /**
- * @returns The version of Olm.
- */
- public static getOlmVersion(): [number, number, number] {
- return OlmDevice.getOlmVersion();
- }
-
- public readonly backupManager: BackupManager;
- public readonly crossSigningInfo: CrossSigningInfo;
- public readonly olmDevice: OlmDevice;
- public readonly deviceList: DeviceList;
- public readonly dehydrationManager: DehydrationManager;
- public readonly secretStorage: SecretStorage;
-
- private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
- private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
- public readonly supportedAlgorithms: string[];
- private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
- private readonly toDeviceVerificationRequests: ToDeviceRequests;
- public readonly inRoomVerificationRequests: InRoomRequests;
-
- private trustCrossSignedDevices = true;
- // the last time we did a check for the number of one-time-keys on the server.
- private lastOneTimeKeyCheck: number | null = null;
- private oneTimeKeyCheckInProgress = false;
-
- // EncryptionAlgorithm instance for each room
- private roomEncryptors = new Map<string, EncryptionAlgorithm>();
- // map from algorithm to DecryptionAlgorithm instance, for each room
- private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
-
- private deviceKeys: Record<string, string> = {}; // type: key
-
- public globalBlacklistUnverifiedDevices = false;
- public globalErrorOnUnknownDevices = true;
-
- // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
- // we received in the current sync.
- private receivedRoomKeyRequests: IncomingRoomKeyRequest[] = [];
- private receivedRoomKeyRequestCancellations: IncomingRoomKeyRequestCancellation[] = [];
- // true if we are currently processing received room key requests
- private processingRoomKeyRequests = false;
- // controls whether device tracking is delayed
- // until calling encryptEvent or trackRoomDevices,
- // or done immediately upon enabling room encryption.
- private lazyLoadMembers = false;
- // in case lazyLoadMembers is true,
- // track if an initial tracking of all the room members
- // has happened for a given room. This is delayed
- // to avoid loading room members as long as possible.
- private roomDeviceTrackingState: { [roomId: string]: Promise<void> } = {};
-
- // The timestamp of the last time we forced establishment
- // of a new session for each device, in milliseconds.
- // {
- // userId: {
- // deviceId: 1234567890000,
- // },
- // }
- // Map: user Id → device Id → timestamp
- private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
- () => new MapWithDefault(() => 0),
- );
-
- // This flag will be unset whilst the client processes a sync response
- // so that we don't start requesting keys until we've actually finished
- // processing the response.
- private sendKeyRequestsImmediately = false;
-
- private oneTimeKeyCount?: number;
- private needsNewFallback?: boolean;
- private fallbackCleanup?: ReturnType<typeof setTimeout>;
-
- /**
- * Cryptography bits
- *
- * This module is internal to the js-sdk; the public API is via MatrixClient.
- *
- * @internal
- *
- * @param baseApis - base matrix api interface
- *
- * @param userId - The user ID for the local user
- *
- * @param deviceId - The identifier for this device.
- *
- * @param clientStore - the MatrixClient data store.
- *
- * @param cryptoStore - storage for the crypto layer.
- *
- * @param roomList - An initialised RoomList object
- *
- * @param verificationMethods - Array of verification methods to use.
- * Each element can either be a string from MatrixClient.verificationMethods
- * or a class that implements a verification method.
- */
- public constructor(
- public readonly baseApis: MatrixClient,
- public readonly userId: string,
- private readonly deviceId: string,
- private readonly clientStore: IStore,
- public readonly cryptoStore: CryptoStore,
- private readonly roomList: RoomList,
- verificationMethods: Array<VerificationMethod | (typeof VerificationBase & { NAME: string })>,
- ) {
- super();
- this.reEmitter = new TypedReEmitter(this);
-
- if (verificationMethods) {
- this.verificationMethods = new Map();
- for (const method of verificationMethods) {
- if (typeof method === "string") {
- if (defaultVerificationMethods[method]) {
- this.verificationMethods.set(
- method,
- <typeof VerificationBase>defaultVerificationMethods[method],
- );
- }
- } else if (method["NAME"]) {
- this.verificationMethods.set(method["NAME"], method as typeof VerificationBase);
- } else {
- logger.warn(`Excluding unknown verification method ${method}`);
- }
- }
- } else {
- this.verificationMethods = new Map(Object.entries(defaultVerificationMethods)) as Map<
- VerificationMethod,
- typeof VerificationBase
- >;
- }
-
- this.backupManager = new BackupManager(baseApis, async () => {
- // try to get key from cache
- const cachedKey = await this.getSessionBackupPrivateKey();
- if (cachedKey) {
- return cachedKey;
- }
-
- // try to get key from secret storage
- const storedKey = await this.getSecret("m.megolm_backup.v1");
-
- if (storedKey) {
- // ensure that the key is in the right format. If not, fix the key and
- // store the fixed version
- const fixedKey = fixBackupKey(storedKey);
- if (fixedKey) {
- const keys = await this.getSecretStorageKey();
- await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
- }
-
- return olmlib.decodeBase64(fixedKey || storedKey);
- }
-
- // try to get key from app
- if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) {
- return this.baseApis.cryptoCallbacks.getBackupKey();
- }
-
- throw new Error("Unable to get private key");
- });
-
- this.olmDevice = new OlmDevice(cryptoStore);
- this.deviceList = new DeviceList(baseApis, cryptoStore, this.olmDevice);
-
- // XXX: This isn't removed at any point, but then none of the event listeners
- // this class sets seem to be removed at any point... :/
- this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
- this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
-
- this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
-
- this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
- baseApis,
- this.deviceId,
- this.cryptoStore,
- );
-
- this.toDeviceVerificationRequests = new ToDeviceRequests();
- this.inRoomVerificationRequests = new InRoomRequests();
-
- const cryptoCallbacks = this.baseApis.cryptoCallbacks || {};
- const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
-
- this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
- // Yes, we pass the client twice here: see SecretStorage
- this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
- this.dehydrationManager = new DehydrationManager(this);
-
- // Assuming no app-supplied callback, default to getting from SSSS.
- if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
- cryptoCallbacks.getCrossSigningKey = async (type): Promise<Uint8Array | null> => {
- return CrossSigningInfo.getFromSecretStorage(type, this.secretStorage);
- };
- }
- }
-
- /**
- * Initialise the crypto module so that it is ready for use
- *
- * Returns a promise which resolves once the crypto module is ready for use.
- *
- * @param exportedOlmDevice - (Optional) data from exported device
- * that must be re-created.
- */
- public async init({ exportedOlmDevice, pickleKey }: IInitOpts = {}): Promise<void> {
- logger.log("Crypto: initialising Olm...");
- await global.Olm.init();
- logger.log(
- exportedOlmDevice
- ? "Crypto: initialising Olm device from exported device..."
- : "Crypto: initialising Olm device...",
- );
- await this.olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey });
- logger.log("Crypto: loading device list...");
- await this.deviceList.load();
-
- // build our device keys: these will later be uploaded
- this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key!;
- this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key!;
-
- logger.log("Crypto: fetching own devices...");
- let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId);
-
- if (!myDevices) {
- myDevices = {};
- }
-
- if (!myDevices[this.deviceId]) {
- // add our own deviceinfo to the cryptoStore
- logger.log("Crypto: adding this device to the store...");
- const deviceInfo = {
- keys: this.deviceKeys,
- algorithms: this.supportedAlgorithms,
- verified: DeviceVerification.VERIFIED,
- known: true,
- };
-
- myDevices[this.deviceId] = deviceInfo;
- this.deviceList.storeDevicesForUser(this.userId, myDevices);
- this.deviceList.saveIfDirty();
- }
-
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getCrossSigningKeys(txn, (keys) => {
- // can be an empty object after resetting cross-signing keys, see storeTrustedSelfKeys
- if (keys && Object.keys(keys).length !== 0) {
- logger.log("Loaded cross-signing public keys from crypto store");
- this.crossSigningInfo.setKeys(keys);
- }
- });
- });
- // make sure we are keeping track of our own devices
- // (this is important for key backups & things)
- this.deviceList.startTrackingDeviceList(this.userId);
-
- logger.log("Crypto: checking for key backup...");
- this.backupManager.checkAndStart();
- }
-
- /**
- * Whether to trust a others users signatures of their devices.
- * If false, devices will only be considered 'verified' if we have
- * verified that device individually (effectively disabling cross-signing).
- *
- * Default: true
- *
- * @returns True if trusting cross-signed devices
- */
- public getCryptoTrustCrossSignedDevices(): boolean {
- return this.trustCrossSignedDevices;
- }
-
- /**
- * See getCryptoTrustCrossSignedDevices
-
- * This may be set before initCrypto() is called to ensure no races occur.
- *
- * @param val - True to trust cross-signed devices
- */
- public setCryptoTrustCrossSignedDevices(val: boolean): void {
- this.trustCrossSignedDevices = val;
-
- for (const userId of this.deviceList.getKnownUserIds()) {
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- for (const deviceId of Object.keys(devices)) {
- const deviceTrust = this.checkDeviceTrust(userId, deviceId);
- // If the device is locally verified then isVerified() is always true,
- // so this will only have caused the value to change if the device is
- // cross-signing verified but not locally verified
- if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) {
- const deviceObj = this.deviceList.getStoredDevice(userId, deviceId)!;
- this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
- }
- }
- }
- }
-
- /**
- * Create a recovery key from a user-supplied passphrase.
- *
- * @param password - Passphrase string that can be entered by the user
- * when restoring the backup as an alternative to entering the recovery key.
- * Optional.
- * @returns Object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- */
- public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
- const decryption = new global.Olm.PkDecryption();
- try {
- const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
- if (password) {
- const derivation = await keyFromPassphrase(password);
- keyInfo.passphrase = {
- algorithm: "m.pbkdf2",
- iterations: derivation.iterations,
- salt: derivation.salt,
- };
- keyInfo.pubkey = decryption.init_with_private_key(derivation.key);
- } else {
- keyInfo.pubkey = decryption.generate_key();
- }
- const privateKey = decryption.get_private_key();
- const encodedPrivateKey = encodeRecoveryKey(privateKey);
- return {
- keyInfo: keyInfo as IRecoveryKey["keyInfo"],
- encodedPrivateKey,
- privateKey,
- };
- } finally {
- decryption?.free();
- }
- }
-
- /**
- * Checks if the user has previously published cross-signing keys
- *
- * This means downloading the devicelist for the user and checking if the list includes
- * the cross-signing pseudo-device.
- *
- * @internal
- */
- public async userHasCrossSigningKeys(): Promise<boolean> {
- await this.downloadKeys([this.userId]);
- return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null;
- }
-
- /**
- * Checks whether cross signing:
- * - is enabled on this account and trusted by this device
- * - has private keys either cached locally or stored in secret storage
- *
- * If this function returns false, bootstrapCrossSigning() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapCrossSigning() completes successfully, this function should
- * return true.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @returns True if cross-signing is ready to be used on this device
- */
- public async isCrossSigningReady(): Promise<boolean> {
- const publicKeysOnDevice = this.crossSigningInfo.getId();
- const privateKeysExistSomewhere =
- (await this.crossSigningInfo.isStoredInKeyCache()) ||
- (await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage));
-
- return !!(publicKeysOnDevice && privateKeysExistSomewhere);
- }
-
- /**
- * Checks whether secret storage:
- * - is enabled on this account
- * - is storing cross-signing private keys
- * - is storing session backup key (if enabled)
- *
- * If this function returns false, bootstrapSecretStorage() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapSecretStorage() completes successfully, this function should
- * return true.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @returns True if secret storage is ready to be used on this device
- */
- public async isSecretStorageReady(): Promise<boolean> {
- const secretStorageKeyInAccount = await this.secretStorage.hasKey();
- const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
- const sessionBackupInStorage =
- !this.backupManager.getKeyBackupEnabled() || (await this.baseApis.isKeyBackupKeyStored());
-
- return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
- }
-
- /**
- * Bootstrap cross-signing by creating keys if needed. If everything is already
- * set up, then no changes are made, so this is safe to run to ensure
- * cross-signing is ready for use.
- *
- * This function:
- * - creates new cross-signing keys if they are not found locally cached nor in
- * secret storage (if it has been setup)
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param authUploadDeviceSigningKeys - Function
- * called to await an interactive auth flow when uploading device signing keys.
- * @param setupNewCrossSigning - Optional. Reset even if keys
- * already exist.
- * Args:
- * A function that makes the request requiring auth. Receives the
- * auth data as an object. Can be called multiple times, first with an empty
- * authDict, to obtain the flows.
- */
- public async bootstrapCrossSigning({
- authUploadDeviceSigningKeys,
- setupNewCrossSigning,
- }: IBootstrapCrossSigningOpts = {}): Promise<void> {
- logger.log("Bootstrapping cross-signing");
-
- const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
- const builder = new EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
- const crossSigningInfo = new CrossSigningInfo(
- this.userId,
- builder.crossSigningCallbacks,
- builder.crossSigningCallbacks,
- );
-
- // Reset the cross-signing keys
- const resetCrossSigning = async (): Promise<void> => {
- crossSigningInfo.resetKeys();
- // Sign master key with device key
- await this.signObject(crossSigningInfo.keys.master);
-
- // Store auth flow helper function, as we need to call it when uploading
- // to ensure we handle auth errors properly.
- builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
-
- // Cross-sign own device
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
- builder.addKeySignature(this.userId, this.deviceId, deviceSignature!);
-
- // Sign message key backup with cross-signing master key
- if (this.backupManager.backupInfo) {
- await crossSigningInfo.signObject(this.backupManager.backupInfo.auth_data, "master");
- builder.addSessionBackup(this.backupManager.backupInfo);
- }
- };
-
- const publicKeysOnDevice = this.crossSigningInfo.getId();
- const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache();
- const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
- const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage;
-
- // Log all relevant state for easier parsing of debug logs.
- logger.log({
- setupNewCrossSigning,
- publicKeysOnDevice,
- privateKeysInCache,
- privateKeysInStorage,
- privateKeysExistSomewhere,
- });
-
- if (!privateKeysExistSomewhere || setupNewCrossSigning) {
- logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys");
- // If a user has multiple devices, it important to only call bootstrap
- // as part of some UI flow (and not silently during startup), as they
- // may have setup cross-signing on a platform which has not saved keys
- // to secret storage, and this would reset them. In such a case, you
- // should prompt the user to verify any existing devices first (and
- // request private keys from those devices) before calling bootstrap.
- await resetCrossSigning();
- } else if (publicKeysOnDevice && privateKeysInCache) {
- logger.log("Cross-signing public keys trusted and private keys found locally");
- } else if (privateKeysInStorage) {
- logger.log(
- "Cross-signing private keys not found locally, but they are available " +
- "in secret storage, reading storage and caching locally",
- );
- await this.checkOwnCrossSigningTrust({
- allowPrivateKeyRequests: true,
- });
- }
-
- // Assuming no app-supplied callback, default to storing new private keys in
- // secret storage if it exists. If it does not, it is assumed this will be
- // done as part of setting up secret storage later.
- const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
- if (crossSigningPrivateKeys.size && !this.baseApis.cryptoCallbacks.saveCrossSigningKeys) {
- const secretStorage = new SecretStorage(
- builder.accountDataClientAdapter,
- builder.ssssCryptoCallbacks,
- undefined,
- );
- if (await secretStorage.hasKey()) {
- logger.log("Storing new cross-signing private keys in secret storage");
- // This is writing to in-memory account data in
- // builder.accountDataClientAdapter so won't fail
- await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
- }
- }
-
- const operation = builder.buildOperation();
- await operation.apply(this);
- // This persists private keys and public keys as trusted,
- // only do this if apply succeeded for now as retry isn't in place yet
- await builder.persist(this);
-
- logger.log("Cross-signing ready");
- }
-
- /**
- * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
- * already set up, then no changes are made, so this is safe to run to ensure secret
- * storage is ready for use.
- *
- * This function
- * - creates a new Secure Secret Storage key if no default key exists
- * - if a key backup exists, it is migrated to store the key in the Secret
- * Storage
- * - creates a backup if none exists, and one is requested
- * - migrates Secure Secret Storage to use the latest algorithm, if an outdated
- * algorithm is found
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param createSecretStorageKey - Optional. Function
- * called to await a secret storage key creation flow.
- * Returns a Promise which resolves to an object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- * @param keyBackupInfo - The current key backup object. If passed,
- * the passphrase and recovery key from this backup will be used.
- * @param setupNewKeyBackup - If true, a new key backup version will be
- * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
- * is supplied.
- * @param setupNewSecretStorage - Optional. Reset even if keys already exist.
- * @param getKeyBackupPassphrase - Optional. Function called to get the user's
- * current key backup passphrase. Should return a promise that resolves with a Buffer
- * containing the key, or rejects if the key cannot be obtained.
- * Returns:
- * A promise which resolves to key creation data for
- * SecretStorage#addKey: an object with `passphrase` etc fields.
- */
- // TODO this does not resolve with what it says it does
- public async bootstrapSecretStorage({
- createSecretStorageKey = async (): Promise<IRecoveryKey> => ({} as IRecoveryKey),
- keyBackupInfo,
- setupNewKeyBackup,
- setupNewSecretStorage,
- getKeyBackupPassphrase,
- }: ICreateSecretStorageOpts = {}): Promise<void> {
- logger.log("Bootstrapping Secure Secret Storage");
- const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
- const builder = new EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
- const secretStorage = new SecretStorage(
- builder.accountDataClientAdapter,
- builder.ssssCryptoCallbacks,
- undefined,
- );
-
- // the ID of the new SSSS key, if we create one
- let newKeyId: string | null = null;
-
- // create a new SSSS key and set it as default
- const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array): Promise<string> => {
- if (privateKey) {
- opts.key = privateKey;
- }
-
- const { keyId, keyInfo } = await secretStorage.addKey(SECRET_STORAGE_ALGORITHM_V1_AES, opts);
-
- if (privateKey) {
- // make the private key available to encrypt 4S secrets
- builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
- }
-
- await secretStorage.setDefaultKeyId(keyId);
- return keyId;
- };
-
- const ensureCanCheckPassphrase = async (keyId: string, keyInfo: SecretStorageKeyDescription): Promise<void> => {
- if (!keyInfo.mac) {
- const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
- { keys: { [keyId]: keyInfo } },
- "",
- );
- if (key) {
- const privateKey = key[1];
- builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
- const { iv, mac } = await calculateKeyCheck(privateKey);
- keyInfo.iv = iv;
- keyInfo.mac = mac;
-
- await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
- }
- }
- };
-
- const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]): Promise<void> => {
- if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) {
- try {
- logger.log("Adding cross-signing signature to key backup");
- await this.crossSigningInfo.signObject(keyBackupAuthData, "master");
- } catch (e) {
- // This step is not critical (just helpful), so we catch here
- // and continue if it fails.
- logger.error("Signing key backup with cross-signing keys failed", e);
- }
- } else {
- logger.warn("Cross-signing keys not available, skipping signature on key backup");
- }
- };
-
- const oldSSSSKey = await this.getSecretStorageKey();
- const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
- const storageExists =
- !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES;
-
- // Log all relevant state for easier parsing of debug logs.
- logger.log({
- keyBackupInfo,
- setupNewKeyBackup,
- setupNewSecretStorage,
- storageExists,
- oldKeyInfo,
- });
-
- if (!storageExists && !keyBackupInfo) {
- // either we don't have anything, or we've been asked to restart
- // from scratch
- logger.log("Secret storage does not exist, creating new storage key");
-
- // if we already have a usable default SSSS key and aren't resetting
- // SSSS just use it. otherwise, create a new one
- // Note: we leave the old SSSS key in place: there could be other
- // secrets using it, in theory. We could move them to the new key but a)
- // that would mean we'd need to prompt for the old passphrase, and b)
- // it's not clear that would be the right thing to do anyway.
- const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
- newKeyId = await createSSSS(keyInfo, privateKey);
- } else if (!storageExists && keyBackupInfo) {
- // we have an existing backup, but no SSSS
- logger.log("Secret storage does not exist, using key backup key");
-
- // if we have the backup key already cached, use it; otherwise use the
- // callback to prompt for the key
- const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.());
-
- // create a new SSSS key and use the backup key as the new SSSS key
- const opts = {} as IAddSecretStorageKeyOpts;
-
- if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) {
- // FIXME: ???
- opts.passphrase = {
- algorithm: "m.pbkdf2",
- iterations: keyBackupInfo.auth_data.private_key_iterations,
- salt: keyBackupInfo.auth_data.private_key_salt,
- bits: 256,
- };
- }
-
- newKeyId = await createSSSS(opts, backupKey);
-
- // store the backup key in secret storage
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey!), [newKeyId]);
-
- // The backup is trusted because the user provided the private key.
- // Sign the backup with the cross-signing key so the key backup can
- // be trusted via cross-signing.
- await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data);
-
- builder.addSessionBackup(keyBackupInfo);
- } else {
- // 4S is already set up
- logger.log("Secret storage exists");
-
- if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- // make sure that the default key has the information needed to
- // check the passphrase
- await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
- }
- }
-
- // If we have cross-signing private keys cached, store them in secret
- // storage if they are not there already.
- if (
- !this.baseApis.cryptoCallbacks.saveCrossSigningKeys &&
- (await this.isCrossSigningReady()) &&
- (newKeyId || !(await this.crossSigningInfo.isStoredInSecretStorage(secretStorage)))
- ) {
- logger.log("Copying cross-signing private keys from cache to secret storage");
- const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache();
- // This is writing to in-memory account data in
- // builder.accountDataClientAdapter so won't fail
- await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
- }
-
- if (setupNewKeyBackup && !keyBackupInfo) {
- logger.log("Creating new message key backup version");
- const info = await this.baseApis.prepareKeyBackupVersion(
- null /* random key */,
- // don't write to secret storage, as it will write to this.secretStorage.
- // Here, we want to capture all the side-effects of bootstrapping,
- // and want to write to the local secretStorage object
- { secureSecretStorage: false },
- );
- // write the key ourselves to 4S
- const privateKey = decodeRecoveryKey(info.recovery_key);
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
-
- // create keyBackupInfo object to add to builder
- const data: IKeyBackupInfo = {
- algorithm: info.algorithm,
- auth_data: info.auth_data,
- };
-
- // Sign with cross-signing master key
- await signKeyBackupWithCrossSigning(data.auth_data);
-
- // sign with the device fingerprint
- await this.signObject(data.auth_data);
-
- builder.addSessionBackup(data);
- }
-
- // Cache the session backup key
- const sessionBackupKey = await secretStorage.get("m.megolm_backup.v1");
- if (sessionBackupKey) {
- logger.info("Got session backup key from secret storage: caching");
- // fix up the backup key if it's in the wrong format, and replace
- // in secret storage
- const fixedBackupKey = fixBackupKey(sessionBackupKey);
- if (fixedBackupKey) {
- const keyId = newKeyId || oldKeyId;
- await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null);
- }
- const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey));
- builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
- } else if (this.backupManager.getKeyBackupEnabled()) {
- // key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
- // the cache or the user can provide one, and if so, write it to SSSS
- const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.());
- if (!backupKey) {
- // This will require user intervention to recover from since we don't have the key
- // backup key anywhere. The user should probably just set up a new key backup and
- // the key for the new backup will be stored. If we hit this scenario in the wild
- // with any frequency, we should do more than just log an error.
- logger.error("Key backup is enabled but couldn't get key backup key!");
- return;
- }
- logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
- }
-
- const operation = builder.buildOperation();
- await operation.apply(this);
- // this persists private keys and public keys as trusted,
- // only do this if apply succeeded for now as retry isn't in place yet
- await builder.persist(this);
-
- logger.log("Secure Secret Storage ready");
- }
-
- public addSecretStorageKey(
- algorithm: string,
- opts: IAddSecretStorageKeyOpts,
- keyID?: string,
- ): Promise<SecretStorageKeyObject> {
- return this.secretStorage.addKey(algorithm, opts, keyID);
- }
-
- public hasSecretStorageKey(keyID?: string): Promise<boolean> {
- return this.secretStorage.hasKey(keyID);
- }
-
- public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple | null> {
- return this.secretStorage.getKey(keyID);
- }
-
- public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
- return this.secretStorage.store(name, secret, keys);
- }
-
- public getSecret(name: string): Promise<string | undefined> {
- return this.secretStorage.get(name);
- }
-
- public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
- return this.secretStorage.isStored(name);
- }
-
- public requestSecret(name: string, devices: string[]): ISecretRequest {
- if (!devices) {
- devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
- }
- return this.secretStorage.request(name, devices);
- }
-
- public getDefaultSecretStorageKeyId(): Promise<string | null> {
- return this.secretStorage.getDefaultKeyId();
- }
-
- public setDefaultSecretStorageKeyId(k: string): Promise<void> {
- return this.secretStorage.setDefaultKeyId(k);
- }
-
- public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
- return this.secretStorage.checkKey(key, info);
- }
-
- /**
- * Checks that a given secret storage private key matches a given public key.
- * This can be used by the getSecretStorageKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- *
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- let decryption: PkDecryption | null = null;
- try {
- decryption = new global.Olm.PkDecryption();
- const gotPubkey = decryption.init_with_private_key(privateKey);
- // make sure it agrees with the given pubkey
- return gotPubkey === expectedPublicKey;
- } finally {
- decryption?.free();
- }
- }
-
- /**
- * Fetches the backup private key, if cached
- * @returns the key, if any, or null
- */
- public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
- let key = await new Promise<any>((resolve) => {
- // TODO types
- this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
- });
- });
-
- // make sure we have a Uint8Array, rather than a string
- if (key && typeof key === "string") {
- key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key));
- await this.storeSessionBackupPrivateKey(key);
- }
- if (key && key.ciphertext) {
- const pickleKey = Buffer.from(this.olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
- key = olmlib.decodeBase64(decrypted);
- }
- return key;
- }
-
- /**
- * Stores the session backup key to the cache
- * @param key - the private key
- * @returns a promise so you can catch failures
- */
- public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
- if (!(key instanceof Uint8Array)) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
- }
- const pickleKey = Buffer.from(this.olmDevice.pickleKey);
- const encryptedKey = await encryptAES(olmlib.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);
- });
- }
-
- /**
- * Checks that a given cross-signing private key matches a given public key.
- * This can be used by the getCrossSigningKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- *
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- let signing: PkSigning | null = null;
- try {
- signing = new global.Olm.PkSigning();
- const gotPubkey = signing.init_with_seed(privateKey);
- // make sure it agrees with the given pubkey
- return gotPubkey === expectedPublicKey;
- } finally {
- signing?.free();
- }
- }
-
- /**
- * Run various follow-up actions after cross-signing keys have changed locally
- * (either by resetting the keys for the account or by getting them from secret
- * storage), such as signing the current device, upgrading device
- * verifications, etc.
- */
- private async afterCrossSigningLocalKeyChange(): Promise<void> {
- logger.info("Starting cross-signing key change post-processing");
-
- // sign the current device with the new key, and upload to the server
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
- logger.info(`Starting background key sig upload for ${this.deviceId}`);
-
- const upload = ({ shouldEmit = false }): Promise<void> => {
- return this.baseApis
- .uploadKeySignatures({
- [this.userId]: {
- [this.deviceId]: signedDevice!,
- },
- })
- .then((response) => {
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "afterCrossSigningLocalKeyChange",
- upload, // continuation
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- logger.info(`Finished background key sig upload for ${this.deviceId}`);
- })
- .catch((e) => {
- logger.error(`Error during background key sig upload for ${this.deviceId}`, e);
- });
- };
- upload({ shouldEmit: true });
-
- const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
- if (shouldUpgradeCb) {
- logger.info("Starting device verification upgrade");
-
- // Check all users for signatures if upgrade callback present
- // FIXME: do this in batches
- const users: Record<string, IDeviceVerificationUpgrade> = {};
- for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) {
- const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
- userId,
- CrossSigningInfo.fromStorage(crossSigningInfo, userId),
- );
- if (upgradeInfo) {
- users[userId] = upgradeInfo;
- }
- }
-
- if (Object.keys(users).length > 0) {
- logger.info(`Found ${Object.keys(users).length} verif users to upgrade`);
- try {
- const usersToUpgrade = await shouldUpgradeCb({ users: users });
- if (usersToUpgrade) {
- for (const userId of usersToUpgrade) {
- if (userId in users) {
- await this.baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId()!);
- }
- }
- }
- } catch (e) {
- logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e);
- }
- }
-
- logger.info("Finished device verification upgrade");
- }
-
- logger.info("Finished cross-signing key change post-processing");
- }
-
- /**
- * Check if a user's cross-signing key is a candidate for upgrading from device
- * verification.
- *
- * @param userId - the user whose cross-signing information is to be checked
- * @param crossSigningInfo - the cross-signing information to check
- */
- private async checkForDeviceVerificationUpgrade(
- userId: string,
- crossSigningInfo: CrossSigningInfo,
- ): Promise<IDeviceVerificationUpgrade | undefined> {
- // only upgrade if this is the first cross-signing key that we've seen for
- // them, and if their cross-signing key isn't already verified
- const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo);
- if (crossSigningInfo.firstUse && !trustLevel.isVerified()) {
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- const deviceIds = await this.checkForValidDeviceSignature(userId, crossSigningInfo.keys.master, devices);
- if (deviceIds.length) {
- return {
- devices: deviceIds.map((deviceId) => DeviceInfo.fromStorage(devices[deviceId], deviceId)),
- crossSigningInfo,
- };
- }
- }
- }
-
- /**
- * Check if the cross-signing key is signed by a verified device.
- *
- * @param userId - the user ID whose key is being checked
- * @param key - the key that is being checked
- * @param devices - the user's devices. Should be a map from device ID
- * to device info
- */
- private async checkForValidDeviceSignature(
- userId: string,
- key: ICrossSigningKey,
- devices: Record<string, IDevice>,
- ): Promise<string[]> {
- const deviceIds: string[] = [];
- if (devices && key.signatures && key.signatures[userId]) {
- for (const signame of Object.keys(key.signatures[userId])) {
- const [, deviceId] = signame.split(":", 2);
- if (deviceId in devices && devices[deviceId].verified === DeviceVerification.VERIFIED) {
- try {
- await olmlib.verifySignature(
- this.olmDevice,
- key,
- userId,
- deviceId,
- devices[deviceId].keys[signame],
- );
- deviceIds.push(deviceId);
- } catch (e) {}
- }
- }
- }
- return deviceIds;
- }
-
- /**
- * Get the user's cross-signing key ID.
- *
- * @param type - The type of key to get the ID of. One of
- * "master", "self_signing", or "user_signing". Defaults to "master".
- *
- * @returns the key ID
- */
- public getCrossSigningId(type: string): string | null {
- return this.crossSigningInfo.getId(type);
- }
-
- /**
- * Get the cross signing information for a given user.
- *
- * @param userId - the user ID to get the cross-signing info for.
- *
- * @returns the cross signing information for the user.
- */
- public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
- return this.deviceList.getStoredCrossSigningForUser(userId);
- }
-
- /**
- * Check whether a given user is trusted.
- *
- * @param userId - The ID of the user to check.
- *
- * @returns
- */
- public checkUserTrust(userId: string): UserTrustLevel {
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (!userCrossSigning) {
- return new UserTrustLevel(false, false, false);
- }
- return this.crossSigningInfo.checkUserTrust(userCrossSigning);
- }
-
- /**
- * Check whether a given device is trusted.
- *
- * @param userId - The ID of the user whose devices is to be checked.
- * @param deviceId - The ID of the device to check
- *
- * @returns
- */
- public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
- const device = this.deviceList.getStoredDevice(userId, deviceId);
- return this.checkDeviceInfoTrust(userId, device);
- }
-
- /**
- * Check whether a given deviceinfo is trusted.
- *
- * @param userId - The ID of the user whose devices is to be checked.
- * @param device - The device info object to check
- *
- * @returns
- */
- public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel {
- const trustedLocally = !!device?.isVerified();
-
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (device && userCrossSigning) {
- // The trustCrossSignedDevices only affects trust of other people's cross-signing
- // signatures
- const trustCrossSig = this.trustCrossSignedDevices || userId === this.userId;
- return this.crossSigningInfo.checkDeviceTrust(userCrossSigning, device, trustedLocally, trustCrossSig);
- } else {
- return new DeviceTrustLevel(false, false, trustedLocally, false);
- }
- }
-
- /**
- * Check whether one of our own devices is cross-signed by our
- * user's stored keys, regardless of whether we trust those keys yet.
- *
- * @param deviceId - The ID of the device to check
- *
- * @returns true if the device is cross-signed
- */
- public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
- const device = this.deviceList.getStoredDevice(this.userId, deviceId);
- if (!device) return false;
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
- return (
- userCrossSigning?.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified() ?? false
- );
- }
-
- /*
- * Event handler for DeviceList's userNewDevices event
- */
- private onDeviceListUserCrossSigningUpdated = async (userId: string): Promise<void> => {
- if (userId === this.userId) {
- // An update to our own cross-signing key.
- // Get the new key first:
- const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null;
- const currentPubkey = this.crossSigningInfo.getId();
- const changed = currentPubkey !== seenPubkey;
-
- if (currentPubkey && seenPubkey && !changed) {
- // If it's not changed, just make sure everything is up to date
- await this.checkOwnCrossSigningTrust();
- } else {
- // We'll now be in a state where cross-signing on the account is not trusted
- // because our locally stored cross-signing keys will not match the ones
- // on the server for our account. So we clear our own stored cross-signing keys,
- // effectively disabling cross-signing until the user gets verified by the device
- // that reset the keys
- this.storeTrustedSelfKeys(null);
- // emit cross-signing has been disabled
- this.emit(CryptoEvent.KeysChanged, {});
- // as the trust for our own user has changed,
- // also emit an event for this
- this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
- }
- } else {
- await this.checkDeviceVerifications(userId);
-
- // Update verified before latch using the current state and save the new
- // latch value in the device list store.
- const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigning) {
- crossSigning.updateCrossSigningVerifiedBefore(this.checkUserTrust(userId).isCrossSigningVerified());
- this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
- }
-
- this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
- }
- };
-
- /**
- * Check the copy of our cross-signing key that we have in the device list and
- * see if we can get the private key. If so, mark it as trusted.
- */
- public async checkOwnCrossSigningTrust({
- allowPrivateKeyRequests = false,
- }: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
- const userId = this.userId;
-
- // Before proceeding, ensure our cross-signing public keys have been
- // downloaded via the device list.
- await this.downloadKeys([this.userId]);
-
- // Also check which private keys are locally cached.
- const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache();
-
- // If we see an update to our own master key, check it against the master
- // key we have and, if it matches, mark it as verified
-
- // First, get the new cross-signing info
- const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (!newCrossSigning) {
- logger.error(
- "Got cross-signing update event for user " + userId + " but no new cross-signing information found!",
- );
- return;
- }
-
- const seenPubkey = newCrossSigning.getId()!;
- const masterChanged = this.crossSigningInfo.getId() !== seenPubkey;
- const masterExistsNotLocallyCached = newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
- if (masterChanged) {
- logger.info("Got new master public key", seenPubkey);
- }
- if (allowPrivateKeyRequests && (masterChanged || masterExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing master private key");
- let signing: PkSigning | null = null;
- // It's important for control flow that we leave any errors alone for
- // higher levels to handle so that e.g. cancelling access properly
- // aborts any larger operation as well.
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey("master", seenPubkey);
- signing = ret[1];
- logger.info("Got cross-signing master private key");
- } finally {
- signing?.free();
- }
- }
-
- const oldSelfSigningId = this.crossSigningInfo.getId("self_signing");
- const oldUserSigningId = this.crossSigningInfo.getId("user_signing");
-
- // Update the version of our keys in our cross-signing object and the local store
- this.storeTrustedSelfKeys(newCrossSigning.keys);
-
- const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing");
- const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing");
-
- const selfSigningExistsNotLocallyCached =
- newCrossSigning.getId("self_signing") && !crossSigningPrivateKeys.has("self_signing");
- const userSigningExistsNotLocallyCached =
- newCrossSigning.getId("user_signing") && !crossSigningPrivateKeys.has("user_signing");
-
- const keySignatures: Record<string, ISignedKey> = {};
-
- if (selfSigningChanged) {
- logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
- }
- if (allowPrivateKeyRequests && (selfSigningChanged || selfSigningExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing self-signing private key");
- let signing: PkSigning | null = null;
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey(
- "self_signing",
- newCrossSigning.getId("self_signing")!,
- );
- signing = ret[1];
- logger.info("Got cross-signing self-signing private key");
- } finally {
- signing?.free();
- }
-
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
- keySignatures[this.deviceId] = signedDevice!;
- }
- if (userSigningChanged) {
- logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
- }
- if (allowPrivateKeyRequests && (userSigningChanged || userSigningExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing user-signing private key");
- let signing: PkSigning | null = null;
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey(
- "user_signing",
- newCrossSigning.getId("user_signing")!,
- );
- signing = ret[1];
- logger.info("Got cross-signing user-signing private key");
- } finally {
- signing?.free();
- }
- }
-
- if (masterChanged) {
- const masterKey = this.crossSigningInfo.keys.master;
- await this.signObject(masterKey);
- const deviceSig = masterKey.signatures![this.userId]["ed25519:" + this.deviceId];
- // Include only the _new_ device signature in the upload.
- // We may have existing signatures from deleted devices, which will cause
- // the entire upload to fail.
- keySignatures[this.crossSigningInfo.getId()!] = Object.assign({} as ISignedKey, masterKey, {
- signatures: {
- [this.userId]: {
- ["ed25519:" + this.deviceId]: deviceSig,
- },
- },
- });
- }
-
- const keysToUpload = Object.keys(keySignatures);
- if (keysToUpload.length) {
- const upload = ({ shouldEmit = false }): Promise<void> => {
- logger.info(`Starting background key sig upload for ${keysToUpload}`);
- return this.baseApis
- .uploadKeySignatures({ [this.userId]: keySignatures })
- .then((response) => {
- const { failures } = response || {};
- logger.info(`Finished background key sig upload for ${keysToUpload}`);
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "checkOwnCrossSigningTrust",
- upload,
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- })
- .catch((e) => {
- logger.error(`Error during background key sig upload for ${keysToUpload}`, e);
- });
- };
- upload({ shouldEmit: true });
- }
-
- this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
-
- if (masterChanged) {
- this.emit(CryptoEvent.KeysChanged, {});
- await this.afterCrossSigningLocalKeyChange();
- }
-
- // Now we may be able to trust our key backup
- await this.backupManager.checkKeyBackup();
- // FIXME: if we previously trusted the backup, should we automatically sign
- // the backup with the new key (if not already signed)?
- }
-
- /**
- * Store a set of keys as our own, trusted, cross-signing keys.
- *
- * @param keys - The new trusted set of keys
- */
- private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
- if (keys) {
- this.crossSigningInfo.setKeys(keys);
- } else {
- this.crossSigningInfo.clearKeys();
- }
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningInfo.keys);
- });
- }
-
- /**
- * Check if the master key is signed by a verified device, and if so, prompt
- * the application to mark it as verified.
- *
- * @param userId - the user ID whose key should be checked
- */
- private async checkDeviceVerifications(userId: string): Promise<void> {
- const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
- if (!shouldUpgradeCb) {
- // Upgrading skipped when callback is not present.
- return;
- }
- logger.info(`Starting device verification upgrade for ${userId}`);
- if (this.crossSigningInfo.keys.user_signing) {
- const crossSigningInfo = this.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigningInfo) {
- const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, crossSigningInfo);
- if (upgradeInfo) {
- const usersToUpgrade = await shouldUpgradeCb({
- users: {
- [userId]: upgradeInfo,
- },
- });
- if (usersToUpgrade.includes(userId)) {
- await this.baseApis.setDeviceVerified(userId, crossSigningInfo.getId()!);
- }
- }
- }
- }
- logger.info(`Finished device verification upgrade for ${userId}`);
- }
-
- /**
- */
- public enableLazyLoading(): void {
- this.lazyLoadMembers = true;
- }
-
- /**
- * Tell the crypto module to register for MatrixClient events which it needs to
- * listen for
- *
- * @param eventEmitter - event source where we can register
- * for event notifications
- */
- public registerEventHandlers(
- eventEmitter: TypedEventEmitter<
- RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
- any
- >,
- ): void {
- eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
- eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
- eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
- eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
- }
-
- /**
- * @deprecated this does nothing and will be removed in a future version
- */
- public start(): void {
- logger.warn("MatrixClient.crypto.start() is deprecated");
- }
-
- /** Stop background processes related to crypto */
- public stop(): void {
- this.outgoingRoomKeyRequestManager.stop();
- this.deviceList.stop();
- this.dehydrationManager.stop();
- }
-
- /**
- * Get the Ed25519 key for this device
- *
- * @returns base64-encoded ed25519 key.
- */
- public getDeviceEd25519Key(): string | null {
- return this.olmDevice.deviceEd25519Key;
- }
-
- /**
- * Get the Curve25519 key for this device
- *
- * @returns base64-encoded curve25519 key.
- */
- public getDeviceCurve25519Key(): string | null {
- return this.olmDevice.deviceCurve25519Key;
- }
-
- /**
- * Set the global override for whether the client should ever send encrypted
- * messages to unverified devices. This provides the default for rooms which
- * do not specify a value.
- *
- * @param value - whether to blacklist all unverified devices by default
- *
- * @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For
- * internal code, set {@link MatrixClient#globalBlacklistUnverifiedDevices} directly.
- */
- public setGlobalBlacklistUnverifiedDevices(value: boolean): void {
- this.globalBlacklistUnverifiedDevices = value;
- }
-
- /**
- * @returns whether to blacklist all unverified devices by default
- *
- * @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For
- * internal code, reference {@link MatrixClient#globalBlacklistUnverifiedDevices} directly.
- */
- public getGlobalBlacklistUnverifiedDevices(): boolean {
- return this.globalBlacklistUnverifiedDevices;
- }
-
- /**
- * Upload the device keys to the homeserver.
- * @returns A promise that will resolve when the keys are uploaded.
- */
- public uploadDeviceKeys(): Promise<IKeysUploadResponse> {
- const deviceKeys = {
- algorithms: this.supportedAlgorithms,
- device_id: this.deviceId,
- keys: this.deviceKeys,
- user_id: this.userId,
- };
-
- return this.signObject(deviceKeys).then(() => {
- return this.baseApis.uploadKeysRequest({
- device_keys: deviceKeys as Required<IDeviceKeys>,
- });
- });
- }
-
- /**
- * Stores the current one_time_key count which will be handled later (in a call of
- * onSyncCompleted). The count is e.g. coming from a /sync response.
- *
- * @param currentCount - The current count of one_time_keys to be stored
- */
- public updateOneTimeKeyCount(currentCount: number): void {
- if (isFinite(currentCount)) {
- this.oneTimeKeyCount = currentCount;
- } else {
- throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number");
- }
- }
-
- public setNeedsNewFallback(needsNewFallback: boolean): void {
- this.needsNewFallback = needsNewFallback;
- }
-
- public getNeedsNewFallback(): boolean {
- return !!this.needsNewFallback;
- }
-
- // check if it's time to upload one-time keys, and do so if so.
- private maybeUploadOneTimeKeys(): void {
- // frequency with which to check & upload one-time keys
- const uploadPeriod = 1000 * 60; // one minute
-
- // max number of keys to upload at once
- // Creating keys can be an expensive operation so we limit the
- // number we generate in one go to avoid blocking the application
- // for too long.
- const maxKeysPerCycle = 5;
-
- if (this.oneTimeKeyCheckInProgress) {
- return;
- }
-
- const now = Date.now();
- if (this.lastOneTimeKeyCheck !== null && now - this.lastOneTimeKeyCheck < uploadPeriod) {
- // we've done a key upload recently.
- return;
- }
-
- this.lastOneTimeKeyCheck = now;
-
- // We need to keep a pool of one time public keys on the server so that
- // other devices can start conversations with us. But we can only store
- // a finite number of private keys in the olm Account object.
- // To complicate things further then can be a delay between a device
- // claiming a public one time key from the server and it sending us a
- // message. We need to keep the corresponding private key locally until
- // we receive the message.
- // But that message might never arrive leaving us stuck with duff
- // private keys clogging up our local storage.
- // So we need some kind of engineering compromise to balance all of
- // these factors.
-
- // Check how many keys we can store in the Account object.
- const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys();
- // Try to keep at most half that number on the server. This leaves the
- // rest of the slots free to hold keys that have been claimed from the
- // server but we haven't received a message for.
- // If we run out of slots when generating new keys then olm will
- // discard the oldest private keys first. This will eventually clean
- // out stale private keys that won't receive a message.
- const keyLimit = Math.floor(maxOneTimeKeys / 2);
-
- const uploadLoop = async (keyCount: number): Promise<void> => {
- while (keyLimit > keyCount || this.getNeedsNewFallback()) {
- // Ask olm to generate new one time keys, then upload them to synapse.
- if (keyLimit > keyCount) {
- logger.info("generating oneTimeKeys");
- const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
- await this.olmDevice.generateOneTimeKeys(keysThisLoop);
- }
-
- if (this.getNeedsNewFallback()) {
- const fallbackKeys = await this.olmDevice.getFallbackKey();
- // if fallbackKeys is non-empty, we've already generated a
- // fallback key, but it hasn't been published yet, so we
- // can use that instead of generating a new one
- if (!fallbackKeys.curve25519 || Object.keys(fallbackKeys.curve25519).length == 0) {
- logger.info("generating fallback key");
- if (this.fallbackCleanup) {
- // cancel any pending fallback cleanup because generating
- // a new fallback key will already drop the old fallback
- // that would have been dropped, and we don't want to kill
- // the current key
- clearTimeout(this.fallbackCleanup);
- delete this.fallbackCleanup;
- }
- await this.olmDevice.generateFallbackKey();
- }
- }
-
- logger.info("calling uploadOneTimeKeys");
- const res = await this.uploadOneTimeKeys();
- if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
- // if the response contains a more up to date value use this
- // for the next loop
- keyCount = res.one_time_key_counts.signed_curve25519;
- } else {
- throw new Error(
- "response for uploading keys does not contain " + "one_time_key_counts.signed_curve25519",
- );
- }
- }
- };
-
- this.oneTimeKeyCheckInProgress = true;
- Promise.resolve()
- .then(() => {
- if (this.oneTimeKeyCount !== undefined) {
- // We already have the current one_time_key count from a /sync response.
- // Use this value instead of asking the server for the current key count.
- return Promise.resolve(this.oneTimeKeyCount);
- }
- // ask the server how many keys we have
- return this.baseApis.uploadKeysRequest({}).then((res) => {
- return res.one_time_key_counts.signed_curve25519 || 0;
- });
- })
- .then((keyCount) => {
- // Start the uploadLoop with the current keyCount. The function checks if
- // we need to upload new keys or not.
- // If there are too many keys on the server then we don't need to
- // create any more keys.
- return uploadLoop(keyCount);
- })
- .catch((e) => {
- logger.error("Error uploading one-time keys", e.stack || e);
- })
- .finally(() => {
- // reset oneTimeKeyCount to prevent start uploading based on old data.
- // it will be set again on the next /sync-response
- this.oneTimeKeyCount = undefined;
- this.oneTimeKeyCheckInProgress = false;
- });
- }
-
- // returns a promise which resolves to the response
- private async uploadOneTimeKeys(): Promise<IKeysUploadResponse> {
- const promises: Promise<unknown>[] = [];
-
- let fallbackJson: Record<string, IOneTimeKey> | undefined;
- if (this.getNeedsNewFallback()) {
- fallbackJson = {};
- const fallbackKeys = await this.olmDevice.getFallbackKey();
- for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
- const k = { key, fallback: true };
- fallbackJson["signed_curve25519:" + keyId] = k;
- promises.push(this.signObject(k));
- }
- this.setNeedsNewFallback(false);
- }
-
- const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
- const oneTimeJson: Record<string, { key: string }> = {};
-
- for (const keyId in oneTimeKeys.curve25519) {
- if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
- const k = {
- key: oneTimeKeys.curve25519[keyId],
- };
- oneTimeJson["signed_curve25519:" + keyId] = k;
- promises.push(this.signObject(k));
- }
- }
-
- await Promise.all(promises);
-
- const requestBody: Record<string, any> = {
- one_time_keys: oneTimeJson,
- };
-
- if (fallbackJson) {
- requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
- requestBody["fallback_keys"] = fallbackJson;
- }
-
- const res = await this.baseApis.uploadKeysRequest(requestBody);
-
- if (fallbackJson) {
- this.fallbackCleanup = setTimeout(() => {
- delete this.fallbackCleanup;
- this.olmDevice.forgetOldFallbackKey();
- }, 60 * 60 * 1000);
- }
-
- await this.olmDevice.markKeysAsPublished();
- return res;
- }
-
- /**
- * Download the keys for a list of users and stores the keys in the session
- * store.
- * @param userIds - The users to fetch.
- * @param forceDownload - Always download the keys even if cached.
- *
- * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`.
- */
- public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
- return this.deviceList.downloadKeys(userIds, !!forceDownload);
- }
-
- /**
- * Get the stored device keys for a user id
- *
- * @param userId - the user to list keys for.
- *
- * @returns list of devices, or null if we haven't
- * managed to get a list of devices for this user yet.
- */
- public getStoredDevicesForUser(userId: string): Array<DeviceInfo> | null {
- return this.deviceList.getStoredDevicesForUser(userId);
- }
-
- /**
- * Get the stored keys for a single device
- *
- *
- * @returns device, or undefined
- * if we don't know about this device
- */
- public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
- return this.deviceList.getStoredDevice(userId, deviceId);
- }
-
- /**
- * Save the device list, if necessary
- *
- * @param delay - Time in ms before which the save actually happens.
- * By default, the save is delayed for a short period in order to batch
- * multiple writes, but this behaviour can be disabled by passing 0.
- *
- * @returns true if the data was saved, false if
- * it was not (eg. because no changes were pending). The promise
- * will only resolve once the data is saved, so may take some time
- * to resolve.
- */
- public saveDeviceList(delay: number): Promise<boolean> {
- return this.deviceList.saveIfDirty(delay);
- }
-
- /**
- * Update the blocked/verified state of the given device
- *
- * @param userId - owner of the device
- * @param deviceId - unique identifier for the device or user's
- * cross-signing public key ID.
- *
- * @param verified - whether to mark the device as verified. Null to
- * leave unchanged.
- *
- * @param blocked - whether to mark the device as blocked. Null to
- * leave unchanged.
- *
- * @param known - whether to mark that the user has been made aware of
- * the existence of this device. Null to leave unchanged
- *
- * @param keys - The list of keys that was present
- * during the device verification. This will be double checked with the list
- * of keys the given device has currently.
- *
- * @returns updated DeviceInfo
- */
- public async setDeviceVerification(
- userId: string,
- deviceId: string,
- verified: boolean | null = null,
- blocked: boolean | null = null,
- known: boolean | null = null,
- keys?: Record<string, string>,
- ): Promise<DeviceInfo | CrossSigningInfo> {
- // Check if the 'device' is actually a cross signing key
- // The js-sdk's verification treats cross-signing keys as devices
- // and so uses this method to mark them verified.
- const xsk = this.deviceList.getStoredCrossSigningForUser(userId);
- if (xsk && xsk.getId() === deviceId) {
- if (blocked !== null || known !== null) {
- throw new Error("Cannot set blocked or known for a cross-signing key");
- }
- if (!verified) {
- throw new Error("Cannot set a cross-signing key as unverified");
- }
- const gotKeyId = keys ? Object.values(keys)[0] : null;
- if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
- throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
- }
-
- if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
- this.storeTrustedSelfKeys(xsk.keys);
- // This will cause our own user trust to change, so emit the event
- this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
- }
-
- // Now sign the master key with our user signing key (unless it's ourself)
- if (userId !== this.userId) {
- logger.info("Master key " + xsk.getId() + " for " + userId + " marked verified. Signing...");
- const device = await this.crossSigningInfo.signUser(xsk);
- if (device) {
- const upload = async ({ shouldEmit = false }): Promise<void> => {
- logger.info("Uploading signature for " + userId + "...");
- const response = await this.baseApis.uploadKeySignatures({
- [userId]: {
- [deviceId]: device,
- },
- });
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "setDeviceVerification",
- upload,
- );
- }
- /* Throwing here causes the process to be cancelled and the other
- * user to be notified */
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- };
- await upload({ shouldEmit: true });
-
- // This will emit events when it comes back down the sync
- // (we could do local echo to speed things up)
- }
- return device as any; // TODO types
- } else {
- return xsk;
- }
- }
-
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- if (!devices || !devices[deviceId]) {
- throw new Error("Unknown device " + userId + ":" + deviceId);
- }
-
- const dev = devices[deviceId];
- let verificationStatus = dev.verified;
-
- if (verified) {
- if (keys) {
- for (const [keyId, key] of Object.entries(keys)) {
- if (dev.keys[keyId] !== key) {
- throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
- }
- }
- }
- verificationStatus = DeviceVerification.VERIFIED;
- } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
- verificationStatus = DeviceVerification.UNVERIFIED;
- }
-
- if (blocked) {
- verificationStatus = DeviceVerification.BLOCKED;
- } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
- verificationStatus = DeviceVerification.UNVERIFIED;
- }
-
- let knownStatus = dev.known;
- if (known !== null) {
- knownStatus = known;
- }
-
- if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
- dev.verified = verificationStatus;
- dev.known = knownStatus;
- this.deviceList.storeDevicesForUser(userId, devices);
- this.deviceList.saveIfDirty();
- }
-
- // do cross-signing
- if (verified && userId === this.userId) {
- logger.info("Own device " + deviceId + " marked verified: signing");
-
- // Signing only needed if other device not already signed
- let device: ISignedKey | undefined;
- const deviceTrust = this.checkDeviceTrust(userId, deviceId);
- if (deviceTrust.isCrossSigningVerified()) {
- logger.log(`Own device ${deviceId} already cross-signing verified`);
- } else {
- device = (await this.crossSigningInfo.signDevice(userId, DeviceInfo.fromStorage(dev, deviceId)))!;
- }
-
- if (device) {
- const upload = async ({ shouldEmit = false }): Promise<void> => {
- logger.info("Uploading signature for " + deviceId);
- const response = await this.baseApis.uploadKeySignatures({
- [userId]: {
- [deviceId]: device!,
- },
- });
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "setDeviceVerification",
- upload, // continuation
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- };
- await upload({ shouldEmit: true });
- // XXX: we'll need to wait for the device list to be updated
- }
- }
-
- const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
- this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
- return deviceObj;
- }
-
- public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
- return this.inRoomVerificationRequests.findRequestInProgress(roomId);
- }
-
- public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
- return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
- }
-
- public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
- const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
- if (existingRequest) {
- return Promise.resolve(existingRequest);
- }
- const channel = new InRoomChannel(this.baseApis, roomId, userId);
- return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
- }
-
- public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
- if (!devices) {
- devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
- }
- const existingRequest = this.toDeviceVerificationRequests.findRequestInProgress(userId, devices);
- if (existingRequest) {
- return Promise.resolve(existingRequest);
- }
- const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
- return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
- }
-
- private async requestVerificationWithChannel(
- userId: string,
- channel: IVerificationChannel,
- requestsMap: IRequestsMap,
- ): Promise<VerificationRequest> {
- let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- // if transaction id is already known, add request
- if (channel.transactionId) {
- requestsMap.setRequestByChannel(channel, request);
- }
- await request.sendRequest();
- // don't replace the request created by a racing remote echo
- const racingRequest = requestsMap.getRequestByChannel(channel);
- if (racingRequest) {
- request = racingRequest;
- } else {
- logger.log(
- `Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`,
- );
- requestsMap.setRequestByChannel(channel, request);
- }
- return request;
- }
-
- public beginKeyVerification(
- method: string,
- userId: string,
- deviceId: string,
- transactionId: string | null = null,
- ): VerificationBase<any, any> {
- let request: Request | undefined;
- if (transactionId) {
- request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
- if (!request) {
- throw new Error(`No request found for user ${userId} with ` + `transactionId ${transactionId}`);
- }
- } else {
- transactionId = ToDeviceChannel.makeTransactionId();
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
- request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
- }
- return request.beginKeyVerification(method, { userId, deviceId });
- }
-
- public async legacyDeviceVerification(
- userId: string,
- deviceId: string,
- method: VerificationMethod,
- ): Promise<VerificationRequest> {
- const transactionId = ToDeviceChannel.makeTransactionId();
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
- const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
- const verifier = request.beginKeyVerification(method, { userId, deviceId });
- // either reject by an error from verify() while sending .start
- // or resolve when the request receives the
- // local (fake remote) echo for sending the .start event
- await Promise.race([verifier.verify(), request.waitFor((r) => r.started)]);
- return request;
- }
-
- /**
- * Get information on the active olm sessions with a user
- * <p>
- * Returns a map from device id to an object with keys 'deviceIdKey' (the
- * device's curve25519 identity key) and 'sessions' (an array of objects in the
- * same format as that returned by
- * {@link OlmDevice#getSessionInfoForDevice}).
- * <p>
- * This method is provided for debugging purposes.
- *
- * @param userId - id of user to inspect
- */
- public async getOlmSessionsForUser(userId: string): Promise<Record<string, IUserOlmSession>> {
- const devices = this.getStoredDevicesForUser(userId) || [];
- const result: { [deviceId: string]: IUserOlmSession } = {};
- for (const device of devices) {
- const deviceKey = device.getIdentityKey();
- const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey);
-
- result[device.deviceId] = {
- deviceIdKey: deviceKey,
- sessions: sessions,
- };
- }
- return result;
- }
-
- /**
- * Get the device which sent an event
- *
- * @param event - event to be checked
- */
- public getEventSenderDeviceInfo(event: MatrixEvent): DeviceInfo | null {
- const senderKey = event.getSenderKey();
- const algorithm = event.getWireContent().algorithm;
-
- if (!senderKey || !algorithm) {
- return null;
- }
-
- if (event.isKeySourceUntrusted()) {
- // we got the key for this event from a source that we consider untrusted
- return null;
- }
-
- // senderKey is the Curve25519 identity key of the device which the event
- // was sent from. In the case of Megolm, it's actually the Curve25519
- // identity key of the device which set up the Megolm session.
-
- const device = this.deviceList.getDeviceByIdentityKey(algorithm, senderKey);
-
- if (device === null) {
- // we haven't downloaded the details of this device yet.
- return null;
- }
-
- // so far so good, but now we need to check that the sender of this event
- // hadn't advertised someone else's Curve25519 key as their own. We do that
- // by checking the Ed25519 claimed by the event (or, in the case of megolm,
- // the event which set up the megolm session), to check that it matches the
- // fingerprint of the purported sending device.
- //
- // (see https://github.com/vector-im/vector-web/issues/2215)
-
- const claimedKey = event.getClaimedEd25519Key();
- if (!claimedKey) {
- logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
- return null;
- }
-
- if (claimedKey !== device.getFingerprint()) {
- logger.warn(
- "Event " +
- event.getId() +
- " claims ed25519 key " +
- claimedKey +
- " but sender device has key " +
- device.getFingerprint(),
- );
- return null;
- }
-
- return device;
- }
-
- /**
- * Get information about the encryption of an event
- *
- * @param event - event to be checked
- *
- * @returns An object with the fields:
- * - encrypted: whether the event is encrypted (if not encrypted, some of the
- * other properties may not be set)
- * - senderKey: the sender's key
- * - algorithm: the algorithm used to encrypt the event
- * - authenticated: whether we can be sure that the owner of the senderKey
- * sent the event
- * - sender: the sender's device information, if available
- * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
- * (only meaningful if `sender` is set)
- */
- public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
- const ret: Partial<IEncryptedEventInfo> = {};
-
- ret.senderKey = event.getSenderKey() ?? undefined;
- ret.algorithm = event.getWireContent().algorithm;
-
- if (!ret.senderKey || !ret.algorithm) {
- ret.encrypted = false;
- return ret as IEncryptedEventInfo;
- }
- ret.encrypted = true;
-
- if (event.isKeySourceUntrusted()) {
- // we got the key this event from somewhere else
- // TODO: check if we can trust the forwarders.
- ret.authenticated = false;
- } else {
- ret.authenticated = true;
- }
-
- // senderKey is the Curve25519 identity key of the device which the event
- // was sent from. In the case of Megolm, it's actually the Curve25519
- // identity key of the device which set up the Megolm session.
-
- ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey) ?? undefined;
-
- // so far so good, but now we need to check that the sender of this event
- // hadn't advertised someone else's Curve25519 key as their own. We do that
- // by checking the Ed25519 claimed by the event (or, in the case of megolm,
- // the event which set up the megolm session), to check that it matches the
- // fingerprint of the purported sending device.
- //
- // (see https://github.com/vector-im/vector-web/issues/2215)
-
- const claimedKey = event.getClaimedEd25519Key();
- if (!claimedKey) {
- logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
- ret.mismatchedSender = true;
- }
-
- if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
- logger.warn(
- "Event " +
- event.getId() +
- " claims ed25519 key " +
- claimedKey +
- "but sender device has key " +
- ret.sender.getFingerprint(),
- );
- ret.mismatchedSender = true;
- }
-
- return ret as IEncryptedEventInfo;
- }
-
- /**
- * Forces the current outbound group session to be discarded such
- * that another one will be created next time an event is sent.
- *
- * @param roomId - The ID of the room to discard the session for
- *
- * This should not normally be necessary.
- */
- public forceDiscardSession(roomId: string): Promise<void> {
- const alg = this.roomEncryptors.get(roomId);
- if (alg === undefined) throw new Error("Room not encrypted");
- if (alg.forceDiscardSession === undefined) {
- throw new Error("Room encryption algorithm doesn't support session discarding");
- }
- alg.forceDiscardSession();
- return Promise.resolve();
- }
-
- /**
- * Configure a room to use encryption (ie, save a flag in the cryptoStore).
- *
- * @param roomId - The room ID to enable encryption in.
- *
- * @param config - The encryption config for the room.
- *
- * @param inhibitDeviceQuery - true to suppress device list query for
- * users in the room (for now). In case lazy loading is enabled,
- * the device query is always inhibited as the members are not tracked.
- *
- * @deprecated It is normally incorrect to call this method directly. Encryption
- * is enabled by receiving an `m.room.encryption` event (which we may have sent
- * previously).
- */
- public async setRoomEncryption(
- roomId: string,
- config: IRoomEncryption,
- inhibitDeviceQuery?: boolean,
- ): Promise<void> {
- const room = this.clientStore.getRoom(roomId);
- if (!room) {
- throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`);
- }
- await this.setRoomEncryptionImpl(room, config);
- if (!this.lazyLoadMembers && !inhibitDeviceQuery) {
- this.deviceList.refreshOutdatedDeviceLists();
- }
- }
-
- /**
- * Set up encryption for a room.
- *
- * This is called when an <tt>m.room.encryption</tt> event is received. It saves a flag
- * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for
- * the room, and enables device-list tracking for the room.
- *
- * It does <em>not</em> initiate a device list query for the room. That is normally
- * done once we finish processing the sync, in onSyncCompleted.
- *
- * @param room - The room to enable encryption in.
- * @param config - The encryption config for the room.
- */
- private async setRoomEncryptionImpl(room: Room, config: IRoomEncryption): Promise<void> {
- const roomId = room.roomId;
-
- // ignore crypto events with no algorithm defined
- // This will happen if a crypto event is redacted before we fetch the room state
- // It would otherwise just throw later as an unknown algorithm would, but we may
- // as well catch this here
- if (!config.algorithm) {
- logger.log("Ignoring setRoomEncryption with no algorithm");
- return;
- }
-
- // if state is being replayed from storage, we might already have a configuration
- // for this room as they are persisted as well.
- // We just need to make sure the algorithm is initialized in this case.
- // However, if the new config is different,
- // we should bail out as room encryption can't be changed once set.
- const existingConfig = this.roomList.getRoomEncryption(roomId);
- if (existingConfig) {
- if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
- logger.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId);
- return;
- }
- }
- // if we already have encryption in this room, we should ignore this event,
- // as it would reset the encryption algorithm.
- // This is at least expected to be called twice, as sync calls onCryptoEvent
- // for both the timeline and state sections in the /sync response,
- // the encryption event would appear in both.
- // If it's called more than twice though,
- // it signals a bug on client or server.
- const existingAlg = this.roomEncryptors.get(roomId);
- if (existingAlg) {
- return;
- }
-
- // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption
- // because it first stores in memory. We should await the promise only
- // after all the in-memory state (roomEncryptors and _roomList) has been updated
- // to avoid races when calling this method multiple times. Hence keep a hold of the promise.
- let storeConfigPromise: Promise<void> | null = null;
- if (!existingConfig) {
- storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
- }
-
- const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
- if (!AlgClass) {
- throw new Error("Unable to encrypt with " + config.algorithm);
- }
-
- const alg = new AlgClass({
- userId: this.userId,
- deviceId: this.deviceId,
- crypto: this,
- olmDevice: this.olmDevice,
- baseApis: this.baseApis,
- roomId,
- config,
- });
- this.roomEncryptors.set(roomId, alg);
-
- if (storeConfigPromise) {
- await storeConfigPromise;
- }
-
- logger.log(`Enabling encryption in ${roomId}`);
-
- // we don't want to force a download of the full membership list of this room, but as soon as we have that
- // list we can start tracking the device list.
- if (room.membersLoaded()) {
- await this.trackRoomDevicesImpl(room);
- } else {
- // wait for the membership list to be loaded
- const onState = (_state: RoomState): void => {
- room.off(RoomStateEvent.Update, onState);
- if (room.membersLoaded()) {
- this.trackRoomDevicesImpl(room).catch((e) => {
- logger.error(`Error enabling device tracking in ${roomId}`, e);
- });
- }
- };
- room.on(RoomStateEvent.Update, onState);
- }
- }
-
- /**
- * Make sure we are tracking the device lists for all users in this room.
- *
- * @param roomId - The room ID to start tracking devices in.
- * @returns when all devices for the room have been fetched and marked to track
- * @deprecated there's normally no need to call this function: device list tracking
- * will be enabled as soon as we have the full membership list.
- */
- public trackRoomDevices(roomId: string): Promise<void> {
- const room = this.clientStore.getRoom(roomId);
- if (!room) {
- throw new Error(`Unable to start tracking devices in unknown room ${roomId}`);
- }
- return this.trackRoomDevicesImpl(room);
- }
-
- /**
- * Make sure we are tracking the device lists for all users in this room.
- *
- * This is normally called when we are about to send an encrypted event, to make sure
- * we have all the devices in the room; but it is also called when processing an
- * m.room.encryption state event (if lazy-loading is disabled), or when members are
- * loaded (if lazy-loading is enabled), to prepare the device list.
- *
- * @param room - Room to enable device-list tracking in
- */
- private trackRoomDevicesImpl(room: Room): Promise<void> {
- const roomId = room.roomId;
- const trackMembers = async (): Promise<void> => {
- // not an encrypted room
- if (!this.roomEncryptors.has(roomId)) {
- return;
- }
- logger.log(`Starting to track devices for room ${roomId} ...`);
- const members = await room.getEncryptionTargetMembers();
- members.forEach((m) => {
- this.deviceList.startTrackingDeviceList(m.userId);
- });
- };
-
- let promise = this.roomDeviceTrackingState[roomId];
- if (!promise) {
- promise = trackMembers();
- this.roomDeviceTrackingState[roomId] = promise.catch((err) => {
- delete this.roomDeviceTrackingState[roomId];
- throw err;
- });
- }
- return promise;
- }
-
- /**
- * Try to make sure we have established olm sessions for all known devices for
- * the given users.
- *
- * @param users - list of user ids
- * @param force - If true, force a new Olm session to be created. Default false.
- *
- * @returns resolves once the sessions are complete, to
- * an Object mapping from userId to deviceId to
- * {@link OlmSessionResult}
- */
- public ensureOlmSessionsForUsers(
- users: string[],
- force?: boolean,
- ): Promise<Map<string, Map<string, olmlib.IOlmSessionResult>>> {
- // map user Id → DeviceInfo[]
- const devicesByUser: Map<string, DeviceInfo[]> = new Map();
-
- for (const userId of users) {
- const userDevices: DeviceInfo[] = [];
- devicesByUser.set(userId, userDevices);
-
- const devices = this.getStoredDevicesForUser(userId) || [];
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother setting up session to ourself
- continue;
- }
- if (deviceInfo.verified == DeviceVerification.BLOCKED) {
- // don't bother setting up sessions with blocked users
- continue;
- }
-
- userDevices.push(deviceInfo);
- }
- }
-
- return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
- }
-
- /**
- * Get a list containing all of the room keys
- *
- * @returns a list of session export objects
- */
- public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
- const exportedSessions: IMegolmSessionData[] = [];
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
- this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
- if (s === null) return;
-
- const sess = this.olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData!);
- delete sess.first_known_index;
- sess.algorithm = olmlib.MEGOLM_ALGORITHM;
- exportedSessions.push(sess);
- });
- });
-
- return exportedSessions;
- }
-
- /**
- * Import a list of room keys previously exported by exportRoomKeys
- *
- * @param keys - a list of session export objects
- * @returns a promise which resolves once the keys have been imported
- */
- public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
- let successes = 0;
- let failures = 0;
- const total = keys.length;
-
- function updateProgress(): void {
- opts.progressCallback?.({
- stage: "load_keys",
- successes,
- failures,
- total,
- });
- }
-
- return Promise.all(
- keys.map((key) => {
- if (!key.room_id || !key.algorithm) {
- logger.warn("ignoring room key entry with missing fields", key);
- failures++;
- if (opts.progressCallback) {
- updateProgress();
- }
- return null;
- }
-
- const alg = this.getRoomDecryptor(key.room_id, key.algorithm);
- return alg.importRoomKey(key, opts).finally(() => {
- successes++;
- if (opts.progressCallback) {
- updateProgress();
- }
- });
- }),
- ).then();
- }
-
- /**
- * Counts the number of end to end session keys that are waiting to be backed up
- * @returns Promise which resolves to the number of sessions requiring backup
- */
- public countSessionsNeedingBackup(): Promise<number> {
- return this.backupManager.countSessionsNeedingBackup();
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- */
- public prepareToEncrypt(room: Room): void {
- const alg = this.roomEncryptors.get(room.roomId);
- if (alg) {
- alg.prepareToEncrypt(room);
- }
- }
-
- /**
- * Encrypt an event according to the configuration of the room.
- *
- * @param event - event to be sent
- *
- * @param room - destination room.
- *
- * @returns Promise which resolves when the event has been
- * encrypted, or null if nothing was needed
- */
- public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
- const roomId = event.getRoomId()!;
-
- const alg = this.roomEncryptors.get(roomId);
- if (!alg) {
- // MatrixClient has already checked that this room should be encrypted,
- // so this is an unexpected situation.
- throw new Error(
- "Room " +
- roomId +
- " was previously configured to use encryption, but is " +
- "no longer. Perhaps the homeserver is hiding the " +
- "configuration event.",
- );
- }
-
- // wait for all the room devices to be loaded
- await this.trackRoomDevicesImpl(room);
-
- let content = event.getContent();
- // If event has an m.relates_to then we need
- // to put this on the wrapping event instead
- const mRelatesTo = content["m.relates_to"];
- if (mRelatesTo) {
- // Clone content here so we don't remove `m.relates_to` from the local-echo
- content = Object.assign({}, content);
- delete content["m.relates_to"];
- }
-
- // Treat element's performance metrics the same as `m.relates_to` (when present)
- const elementPerfMetrics = content["io.element.performance_metrics"];
- if (elementPerfMetrics) {
- content = Object.assign({}, content);
- delete content["io.element.performance_metrics"];
- }
-
- const encryptedContent = (await alg.encryptMessage(room, event.getType(), content)) as IContent;
-
- if (mRelatesTo) {
- encryptedContent["m.relates_to"] = mRelatesTo;
- }
- if (elementPerfMetrics) {
- encryptedContent["io.element.performance_metrics"] = elementPerfMetrics;
- }
-
- event.makeEncrypted(
- "m.room.encrypted",
- encryptedContent,
- this.olmDevice.deviceCurve25519Key!,
- this.olmDevice.deviceEd25519Key!,
- );
- }
-
- /**
- * Decrypt a received event
- *
- *
- * @returns resolves once we have
- * finished decrypting. Rejects with an `algorithms.DecryptionError` if there
- * is a problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- if (event.isRedacted()) {
- // Try to decrypt the redaction event, to support encrypted
- // redaction reasons. If we can't decrypt, just fall back to using
- // the original redacted_because.
- const redactionEvent = new MatrixEvent({
- room_id: event.getRoomId(),
- ...event.getUnsigned().redacted_because,
- });
- let redactedBecause: IEvent = event.getUnsigned().redacted_because!;
- if (redactionEvent.isEncrypted()) {
- try {
- const decryptedEvent = await this.decryptEvent(redactionEvent);
- redactedBecause = decryptedEvent.clearEvent as IEvent;
- } catch (e) {
- logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e);
- }
- }
-
- return {
- clearEvent: {
- room_id: event.getRoomId(),
- type: "m.room.message",
- content: {},
- unsigned: {
- redacted_because: redactedBecause,
- },
- },
- };
- } else {
- const content = event.getWireContent();
- const alg = this.getRoomDecryptor(event.getRoomId()!, content.algorithm);
- return alg.decryptEvent(event);
- }
- }
-
- /**
- * Handle the notification from /sync or /keys/changes that device lists have
- * been changed.
- *
- * @param syncData - Object containing sync tokens associated with this sync
- * @param syncDeviceLists - device_lists field from /sync, or response from
- * /keys/changes
- */
- public async handleDeviceListChanges(
- syncData: ISyncStateData,
- syncDeviceLists: Required<ISyncResponse>["device_lists"],
- ): Promise<void> {
- // Initial syncs don't have device change lists. We'll either get the complete list
- // of changes for the interval or will have invalidated everything in willProcessSync
- if (!syncData.oldSyncToken) return;
-
- // Here, we're relying on the fact that we only ever save the sync data after
- // sucessfully saving the device list data, so we're guaranteed that the device
- // list store is at least as fresh as the sync token from the sync store, ie.
- // any device changes received in sync tokens prior to the 'next' token here
- // have been processed and are reflected in the current device list.
- // If we didn't make this assumption, we'd have to use the /keys/changes API
- // to get key changes between the sync token in the device list and the 'old'
- // sync token used here to make sure we didn't miss any.
- await this.evalDeviceListChanges(syncDeviceLists);
- }
-
- /**
- * Send a request for some room keys, if we have not already done so
- *
- * @param resend - whether to resend the key request if there is
- * already one
- *
- * @returns a promise that resolves when the key request is queued
- */
- public requestRoomKey(
- requestBody: IRoomKeyRequestBody,
- recipients: IRoomKeyRequestRecipient[],
- resend = false,
- ): Promise<void> {
- return this.outgoingRoomKeyRequestManager
- .queueRoomKeyRequest(requestBody, recipients, resend)
- .then(() => {
- if (this.sendKeyRequestsImmediately) {
- this.outgoingRoomKeyRequestManager.sendQueuedRequests();
- }
- })
- .catch((e) => {
- // this normally means we couldn't talk to the store
- logger.error("Error requesting key for event", e);
- });
- }
-
- /**
- * Cancel any earlier room key request
- *
- * @param requestBody - parameters to match for cancellation
- */
- public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): void {
- this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch((e) => {
- logger.warn("Error clearing pending room key requests", e);
- });
- }
-
- /**
- * Re-send any outgoing key requests, eg after verification
- * @returns
- */
- public async cancelAndResendAllOutgoingKeyRequests(): Promise<void> {
- await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests();
- }
-
- /**
- * handle an m.room.encryption event
- *
- * @param room - in which the event was received
- * @param event - encryption event to be processed
- */
- public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
- const content = event.getContent<IRoomEncryption>();
- await this.setRoomEncryptionImpl(room, content);
- }
-
- /**
- * Called before the result of a sync is processed
- *
- * @param syncData - the data from the 'MatrixClient.sync' event
- */
- public async onSyncWillProcess(syncData: ISyncStateData): Promise<void> {
- if (!syncData.oldSyncToken) {
- // If there is no old sync token, we start all our tracking from
- // scratch, so mark everything as untracked. onCryptoEvent will
- // be called for all e2e rooms during the processing of the sync,
- // at which point we'll start tracking all the users of that room.
- logger.log("Initial sync performed - resetting device tracking state");
- this.deviceList.stopTrackingAllDeviceLists();
- // we always track our own device list (for key backups etc)
- this.deviceList.startTrackingDeviceList(this.userId);
- this.roomDeviceTrackingState = {};
- }
-
- this.sendKeyRequestsImmediately = false;
- }
-
- /**
- * handle the completion of a /sync
- *
- * This is called after the processing of each successful /sync response.
- * It is an opportunity to do a batch process on the information received.
- *
- * @param syncData - the data from the 'MatrixClient.sync' event
- */
- public async onSyncCompleted(syncData: OnSyncCompletedData): Promise<void> {
- this.deviceList.setSyncToken(syncData.nextSyncToken ?? null);
- this.deviceList.saveIfDirty();
-
- // we always track our own device list (for key backups etc)
- this.deviceList.startTrackingDeviceList(this.userId);
-
- this.deviceList.refreshOutdatedDeviceLists();
-
- // we don't start uploading one-time keys until we've caught up with
- // to-device messages, to help us avoid throwing away one-time-keys that we
- // are about to receive messages for
- // (https://github.com/vector-im/element-web/issues/2782).
- if (!syncData.catchingUp) {
- this.maybeUploadOneTimeKeys();
- this.processReceivedRoomKeyRequests();
-
- // likewise don't start requesting keys until we've caught up
- // on to_device messages, otherwise we'll request keys that we're
- // just about to get.
- this.outgoingRoomKeyRequestManager.sendQueuedRequests();
-
- // Sync has finished so send key requests straight away.
- this.sendKeyRequestsImmediately = true;
- }
- }
-
- /**
- * Trigger the appropriate invalidations and removes for a given
- * device list
- *
- * @param deviceLists - device_lists field from /sync, or response from
- * /keys/changes
- */
- private async evalDeviceListChanges(deviceLists: Required<ISyncResponse>["device_lists"]): Promise<void> {
- if (Array.isArray(deviceLists?.changed)) {
- deviceLists.changed.forEach((u) => {
- this.deviceList.invalidateUserDeviceList(u);
- });
- }
-
- if (Array.isArray(deviceLists?.left) && deviceLists.left.length) {
- // Check we really don't share any rooms with these users
- // any more: the server isn't required to give us the
- // exact correct set.
- const e2eUserIds = new Set(await this.getTrackedE2eUsers());
-
- deviceLists.left.forEach((u) => {
- if (!e2eUserIds.has(u)) {
- this.deviceList.stopTrackingDeviceList(u);
- }
- });
- }
- }
-
- /**
- * Get a list of all the IDs of users we share an e2e room with
- * for which we are tracking devices already
- *
- * @returns List of user IDs
- */
- private async getTrackedE2eUsers(): Promise<string[]> {
- const e2eUserIds: string[] = [];
- for (const room of this.getTrackedE2eRooms()) {
- const members = await room.getEncryptionTargetMembers();
- for (const member of members) {
- e2eUserIds.push(member.userId);
- }
- }
- return e2eUserIds;
- }
-
- /**
- * Get a list of the e2e-enabled rooms we are members of,
- * and for which we are already tracking the devices
- *
- * @returns
- */
- private getTrackedE2eRooms(): Room[] {
- return this.clientStore.getRooms().filter((room) => {
- // check for rooms with encryption enabled
- const alg = this.roomEncryptors.get(room.roomId);
- if (!alg) {
- return false;
- }
- if (!this.roomDeviceTrackingState[room.roomId]) {
- return false;
- }
-
- // ignore any rooms which we have left
- const myMembership = room.getMyMembership();
- return myMembership === "join" || myMembership === "invite";
- });
- }
-
- /**
- * Encrypts and sends a given object via Olm to-device messages to a given
- * set of devices.
- * @param userDeviceInfoArr - the devices to send to
- * @param payload - fields to include in the encrypted payload
- * @returns Promise which
- * resolves once the message has been encrypted and sent to the given
- * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
- * of the successfully sent messages.
- */
- public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
- const toDeviceBatch: ToDeviceBatch = {
- eventType: EventType.RoomMessageEncrypted,
- batch: [],
- };
-
- try {
- await Promise.all(
- userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
- const deviceId = deviceInfo.deviceId;
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
-
- toDeviceBatch.batch.push({
- userId,
- deviceId,
- payload: encryptedContent,
- });
-
- await olmlib.ensureOlmSessionsForDevices(
- this.olmDevice,
- this.baseApis,
- new Map([[userId, [deviceInfo]]]),
- );
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- deviceInfo,
- payload,
- );
- }),
- );
-
- // prune out any devices that encryptMessageForDevice could not encrypt for,
- // in which case it will have just not added anything to the ciphertext object.
- // There's no point sending messages to devices if we couldn't encrypt to them,
- // since that's effectively a blank message.
- toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
- if (Object.keys(msg.payload.ciphertext).length > 0) {
- return true;
- } else {
- logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
- return false;
- }
- });
-
- try {
- await this.baseApis.queueToDevice(toDeviceBatch);
- } catch (e) {
- logger.error("sendToDevice failed", e);
- throw e;
- }
- } catch (e) {
- logger.error("encryptAndSendToDevices promises failed", e);
- throw e;
- }
- }
-
- private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => {
- try {
- this.onRoomMembership(event, member, oldMembership);
- } catch (e) {
- logger.error("Error handling membership change:", e);
- }
- };
-
- public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
- // all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
- // happens later in decryptEvent, via the EventMapper
- return events.filter((toDevice) => {
- if (
- toDevice.type === EventType.RoomMessageEncrypted &&
- !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
- ) {
- logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
- return false;
- }
- return true;
- });
- }
-
- public preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void> {
- const currentCount = oneTimeKeysCounts.get("signed_curve25519") || 0;
- this.updateOneTimeKeyCount(currentCount);
- return Promise.resolve();
- }
-
- public preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void> {
- this.setNeedsNewFallback(!unusedFallbackKeys.has("signed_curve25519"));
- return Promise.resolve();
- }
-
- private onToDeviceEvent = (event: MatrixEvent): void => {
- try {
- logger.log(
- `received to-device ${event.getType()} from: ` +
- `${event.getSender()} id: ${event.getContent()[ToDeviceMessageId]}`,
- );
-
- if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") {
- this.onRoomKeyEvent(event);
- } else if (event.getType() == "m.room_key_request") {
- this.onRoomKeyRequestEvent(event);
- } else if (event.getType() === "m.secret.request") {
- this.secretStorage.onRequestReceived(event);
- } else if (event.getType() === "m.secret.send") {
- this.secretStorage.onSecretReceived(event);
- } else if (event.getType() === "m.room_key.withheld") {
- this.onRoomKeyWithheldEvent(event);
- } else if (event.getContent().transaction_id) {
- this.onKeyVerificationMessage(event);
- } else if (event.getContent().msgtype === "m.bad.encrypted") {
- this.onToDeviceBadEncrypted(event);
- } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
- if (!event.isBeingDecrypted()) {
- event.attemptDecryption(this);
- }
- // once the event has been decrypted, try again
- event.once(MatrixEventEvent.Decrypted, (ev) => {
- this.onToDeviceEvent(ev);
- });
- }
- } catch (e) {
- logger.error("Error handling toDeviceEvent:", e);
- }
- };
-
- /**
- * Handle a key event
- *
- * @internal
- * @param event - key event
- */
- private onRoomKeyEvent(event: MatrixEvent): void {
- const content = event.getContent();
-
- if (!content.room_id || !content.algorithm) {
- logger.error("key event is missing fields");
- return;
- }
-
- if (!this.backupManager.checkedForBackup) {
- // don't bother awaiting on this - the important thing is that we retry if we
- // haven't managed to check before
- this.backupManager.checkAndStart();
- }
-
- const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
- alg.onRoomKeyEvent(event);
- }
-
- /**
- * Handle a key withheld event
- *
- * @internal
- * @param event - key withheld event
- */
- private onRoomKeyWithheldEvent(event: MatrixEvent): void {
- const content = event.getContent();
-
- if (
- (content.code !== "m.no_olm" && (!content.room_id || !content.session_id)) ||
- !content.algorithm ||
- !content.sender_key
- ) {
- logger.error("key withheld event is missing fields");
- return;
- }
-
- logger.info(
- `Got room key withheld event from ${event.getSender()} ` +
- `for ${content.algorithm} session ${content.sender_key}|${content.session_id} ` +
- `in room ${content.room_id} with code ${content.code} (${content.reason})`,
- );
-
- const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
- if (alg.onRoomKeyWithheldEvent) {
- alg.onRoomKeyWithheldEvent(event);
- }
- if (!content.room_id) {
- // retry decryption for all events sent by the sender_key. This will
- // update the events to show a message indicating that the olm session was
- // wedged.
- const roomDecryptors = this.getRoomDecryptors(content.algorithm);
- for (const decryptor of roomDecryptors) {
- decryptor.retryDecryptionFromSender(content.sender_key);
- }
- }
- }
-
- /**
- * Handle a general key verification event.
- *
- * @internal
- * @param event - verification start event
- */
- private onKeyVerificationMessage(event: MatrixEvent): void {
- if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
- return;
- }
- const createRequest = (event: MatrixEvent): VerificationRequest | undefined => {
- if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
- return;
- }
- const content = event.getContent();
- const deviceId = content && content.from_device;
- if (!deviceId) {
- return;
- }
- const userId = event.getSender()!;
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId]);
- return new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- };
- this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
- }
-
- /**
- * Handle key verification requests sent as timeline events
- *
- * @internal
- * @param event - the timeline event
- * @param room - not used
- * @param atStart - not used
- * @param removed - not used
- * @param whether - this is a live event
- */
- private onTimelineEvent = (
- event: MatrixEvent,
- room: Room,
- atStart: boolean,
- removed: boolean,
- { liveEvent = true } = {},
- ): void => {
- if (!InRoomChannel.validateEvent(event, this.baseApis)) {
- return;
- }
- const createRequest = (event: MatrixEvent): VerificationRequest => {
- const channel = new InRoomChannel(this.baseApis, event.getRoomId()!);
- return new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- };
- this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
- };
-
- private async handleVerificationEvent(
- event: MatrixEvent,
- requestsMap: IRequestsMap,
- createRequest: (event: MatrixEvent) => VerificationRequest | undefined,
- isLiveEvent = true,
- ): Promise<void> {
- // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
- if (event.isSending() && event.status != EventStatus.SENT) {
- let eventIdListener: () => void;
- let statusListener: () => void;
- try {
- await new Promise<void>((resolve, reject) => {
- eventIdListener = resolve;
- statusListener = (): void => {
- if (event.status == EventStatus.CANCELLED) {
- reject(new Error("Event status set to CANCELLED."));
- }
- };
- event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
- event.on(MatrixEventEvent.Status, statusListener);
- });
- } catch (err) {
- logger.error("error while waiting for the verification event to be sent: ", err);
- return;
- } finally {
- event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener!);
- event.removeListener(MatrixEventEvent.Status, statusListener!);
- }
- }
- let request: VerificationRequest | undefined = requestsMap.getRequest(event);
- let isNewRequest = false;
- if (!request) {
- request = createRequest(event);
- // a request could not be made from this event, so ignore event
- if (!request) {
- logger.log(
- `Crypto: could not find VerificationRequest for ` +
- `${event.getType()}, and could not create one, so ignoring.`,
- );
- return;
- }
- isNewRequest = true;
- requestsMap.setRequest(event, request);
- }
- event.setVerificationRequest(request);
- try {
- await request.channel.handleEvent(event, request, isLiveEvent);
- } catch (err) {
- logger.error("error while handling verification event", err);
- }
- const shouldEmit =
- isNewRequest &&
- !request.initiatedByMe &&
- !request.invalid && // check it has enough events to pass the UNSENT stage
- !request.observeOnly;
- if (shouldEmit) {
- this.baseApis.emit(CryptoEvent.VerificationRequest, request);
- }
- }
-
- /**
- * Handle a toDevice event that couldn't be decrypted
- *
- * @internal
- * @param event - undecryptable event
- */
- private async onToDeviceBadEncrypted(event: MatrixEvent): Promise<void> {
- const content = event.getWireContent();
- const sender = event.getSender();
- const algorithm = content.algorithm;
- const deviceKey = content.sender_key;
-
- this.baseApis.emit(ClientEvent.UndecryptableToDeviceEvent, event);
-
- // retry decryption for all events sent by the sender_key. This will
- // update the events to show a message indicating that the olm session was
- // wedged.
- const retryDecryption = (): void => {
- const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM);
- for (const decryptor of roomDecryptors) {
- decryptor.retryDecryptionFromSender(deviceKey);
- }
- };
-
- if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
- return;
- }
-
- // check when we last forced a new session with this device: if we've already done so
- // recently, don't do it again.
- const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
- const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
- if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
- logger.debug(
- "New session already forced with device " +
- sender +
- ":" +
- deviceKey +
- " at " +
- lastNewSessionForced +
- ": not forcing another",
- );
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
- retryDecryption();
- return;
- }
-
- // establish a new olm session with this device since we're failing to decrypt messages
- // on a current session.
- // Note that an undecryptable message from another device could easily be spoofed -
- // is there anything we can do to mitigate this?
- let device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
- if (!device) {
- // if we don't know about the device, fetch the user's devices again
- // and retry before giving up
- await this.downloadKeys([sender], false);
- device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
- if (!device) {
- logger.info("Couldn't find device for identity key " + deviceKey + ": not re-establishing session");
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", false);
- retryDecryption();
- return;
- }
- }
- const devicesByUser = new Map([[sender, [device]]]);
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
-
- lastNewSessionDevices.set(deviceKey, Date.now());
-
- // Now send a blank message on that session so the other side knows about it.
- // (The keyshare request is sent in the clear so that won't do)
- // We send this first such that, as long as the toDevice messages arrive in the
- // same order we sent them, the other end will get this first, set up the new session,
- // then get the keyshare request and send the key over this new session (because it
- // is the session it has most recently received a message on).
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- sender,
- device,
- { type: "m.dummy" },
- );
-
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
- retryDecryption();
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
- );
-
- // Most of the time this probably won't be necessary since we'll have queued up a key request when
- // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
- // it. This won't always be the case though so we need to re-send any that have already been sent
- // to avoid races.
- const requestsToResend = await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
- sender,
- device.deviceId,
- );
- for (const keyReq of requestsToResend) {
- this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
- }
- }
-
- /**
- * Handle a change in the membership state of a member of a room
- *
- * @internal
- * @param event - event causing the change
- * @param member - user whose membership changed
- * @param oldMembership - previous membership
- */
- private onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
- // this event handler is registered on the *client* (as opposed to the room
- // member itself), which means it is only called on changes to the *live*
- // membership state (ie, it is not called when we back-paginate, nor when
- // we load the state in the initialsync).
- //
- // Further, it is automatically registered and called when new members
- // arrive in the room.
-
- const roomId = member.roomId;
-
- const alg = this.roomEncryptors.get(roomId);
- if (!alg) {
- // not encrypting in this room
- return;
- }
- // only mark users in this room as tracked if we already started tracking in this room
- // this way we don't start device queries after sync on behalf of this room which we won't use
- // the result of anyway, as we'll need to do a query again once all the members are fetched
- // by calling _trackRoomDevices
- if (roomId in this.roomDeviceTrackingState) {
- if (member.membership == "join") {
- logger.log("Join event for " + member.userId + " in " + roomId);
- // make sure we are tracking the deviceList for this user
- this.deviceList.startTrackingDeviceList(member.userId);
- } else if (
- member.membership == "invite" &&
- this.clientStore.getRoom(roomId)?.shouldEncryptForInvitedMembers()
- ) {
- logger.log("Invite event for " + member.userId + " in " + roomId);
- this.deviceList.startTrackingDeviceList(member.userId);
- }
- }
-
- alg.onRoomMembership(event, member, oldMembership);
- }
-
- /**
- * Called when we get an m.room_key_request event.
- *
- * @internal
- * @param event - key request event
- */
- private onRoomKeyRequestEvent(event: MatrixEvent): void {
- const content = event.getContent();
- if (content.action === "request") {
- // Queue it up for now, because they tend to arrive before the room state
- // events at initial sync, and we want to see if we know anything about the
- // room before passing them on to the app.
- const req = new IncomingRoomKeyRequest(event);
- this.receivedRoomKeyRequests.push(req);
- } else if (content.action === "request_cancellation") {
- const req = new IncomingRoomKeyRequestCancellation(event);
- this.receivedRoomKeyRequestCancellations.push(req);
- }
- }
-
- /**
- * Process any m.room_key_request events which were queued up during the
- * current sync.
- *
- * @internal
- */
- private async processReceivedRoomKeyRequests(): Promise<void> {
- if (this.processingRoomKeyRequests) {
- // we're still processing last time's requests; keep queuing new ones
- // up for now.
- return;
- }
- this.processingRoomKeyRequests = true;
-
- try {
- // we need to grab and clear the queues in the synchronous bit of this method,
- // so that we don't end up racing with the next /sync.
- const requests = this.receivedRoomKeyRequests;
- this.receivedRoomKeyRequests = [];
- const cancellations = this.receivedRoomKeyRequestCancellations;
- this.receivedRoomKeyRequestCancellations = [];
-
- // Process all of the requests, *then* all of the cancellations.
- //
- // This makes sure that if we get a request and its cancellation in the
- // same /sync result, then we process the request before the
- // cancellation (and end up with a cancelled request), rather than the
- // cancellation before the request (and end up with an outstanding
- // request which should have been cancelled.)
- await Promise.all(requests.map((req) => this.processReceivedRoomKeyRequest(req)));
- await Promise.all(
- cancellations.map((cancellation) => this.processReceivedRoomKeyRequestCancellation(cancellation)),
- );
- } catch (e) {
- logger.error(`Error processing room key requsts: ${e}`);
- } finally {
- this.processingRoomKeyRequests = false;
- }
- }
-
- /**
- * Helper for processReceivedRoomKeyRequests
- *
- */
- private async processReceivedRoomKeyRequest(req: IncomingRoomKeyRequest): Promise<void> {
- const userId = req.userId;
- const deviceId = req.deviceId;
-
- const body = req.requestBody;
- const roomId = body.room_id;
- const alg = body.algorithm;
-
- logger.log(
- `m.room_key_request from ${userId}:${deviceId}` +
- ` for ${roomId} / ${body.session_id} (id ${req.requestId})`,
- );
-
- if (userId !== this.userId) {
- if (!this.roomEncryptors.get(roomId)) {
- logger.debug(`room key request for unencrypted room ${roomId}`);
- return;
- }
- const encryptor = this.roomEncryptors.get(roomId)!;
- const device = this.deviceList.getStoredDevice(userId, deviceId);
- if (!device) {
- logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
- return;
- }
-
- try {
- await encryptor.reshareKeyWithDevice!(body.sender_key, body.session_id, userId, device);
- } catch (e) {
- logger.warn(
- "Failed to re-share keys for session " +
- body.session_id +
- " with device " +
- userId +
- ":" +
- device.deviceId,
- e,
- );
- }
- return;
- }
-
- if (deviceId === this.deviceId) {
- // We'll always get these because we send room key requests to
- // '*' (ie. 'all devices') which includes the sending device,
- // so ignore requests from ourself because apart from it being
- // very silly, it won't work because an Olm session cannot send
- // messages to itself.
- // The log here is probably superfluous since we know this will
- // always happen, but let's log anyway for now just in case it
- // causes issues.
- logger.log("Ignoring room key request from ourselves");
- return;
- }
-
- // todo: should we queue up requests we don't yet have keys for,
- // in case they turn up later?
-
- // if we don't have a decryptor for this room/alg, we don't have
- // the keys for the requested events, and can drop the requests.
- if (!this.roomDecryptors.has(roomId)) {
- logger.log(`room key request for unencrypted room ${roomId}`);
- return;
- }
-
- const decryptor = this.roomDecryptors.get(roomId)!.get(alg);
- if (!decryptor) {
- logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
- return;
- }
-
- if (!(await decryptor.hasKeysForKeyRequest(req))) {
- logger.log(`room key request for unknown session ${roomId} / ` + body.session_id);
- return;
- }
-
- req.share = (): void => {
- decryptor.shareKeysWithDevice(req);
- };
-
- // if the device is verified already, share the keys
- if (this.checkDeviceTrust(userId, deviceId).isVerified()) {
- logger.log("device is already verified: sharing keys");
- req.share();
- return;
- }
-
- this.emit(CryptoEvent.RoomKeyRequest, req);
- }
-
- /**
- * Helper for processReceivedRoomKeyRequests
- *
- */
- private async processReceivedRoomKeyRequestCancellation(
- cancellation: IncomingRoomKeyRequestCancellation,
- ): Promise<void> {
- logger.log(
- `m.room_key_request cancellation for ${cancellation.userId}:` +
- `${cancellation.deviceId} (id ${cancellation.requestId})`,
- );
-
- // we should probably only notify the app of cancellations we told it
- // about, but we don't currently have a record of that, so we just pass
- // everything through.
- this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
- }
-
- /**
- * Get a decryptor for a given room and algorithm.
- *
- * If we already have a decryptor for the given room and algorithm, return
- * it. Otherwise try to instantiate it.
- *
- * @internal
- *
- * @param roomId - room id for decryptor. If undefined, a temporary
- * decryptor is instantiated.
- *
- * @param algorithm - crypto algorithm
- *
- * @throws {@link DecryptionError} if the algorithm is unknown
- */
- public getRoomDecryptor(roomId: string | null, algorithm: string): DecryptionAlgorithm {
- let decryptors: Map<string, DecryptionAlgorithm> | undefined;
- let alg: DecryptionAlgorithm | undefined;
-
- if (roomId) {
- decryptors = this.roomDecryptors.get(roomId);
- if (!decryptors) {
- decryptors = new Map<string, DecryptionAlgorithm>();
- this.roomDecryptors.set(roomId, decryptors);
- }
-
- alg = decryptors.get(algorithm);
- if (alg) {
- return alg;
- }
- }
-
- const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
- if (!AlgClass) {
- throw new algorithms.DecryptionError(
- "UNKNOWN_ENCRYPTION_ALGORITHM",
- 'Unknown encryption algorithm "' + algorithm + '".',
- );
- }
- alg = new AlgClass({
- userId: this.userId,
- crypto: this,
- olmDevice: this.olmDevice,
- baseApis: this.baseApis,
- roomId: roomId ?? undefined,
- });
-
- if (decryptors) {
- decryptors.set(algorithm, alg);
- }
- return alg;
- }
-
- /**
- * Get all the room decryptors for a given encryption algorithm.
- *
- * @param algorithm - The encryption algorithm
- *
- * @returns An array of room decryptors
- */
- private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
- const decryptors: DecryptionAlgorithm[] = [];
- for (const d of this.roomDecryptors.values()) {
- if (d.has(algorithm)) {
- decryptors.push(d.get(algorithm)!);
- }
- }
- return decryptors;
- }
-
- /**
- * sign the given object with our ed25519 key
- *
- * @param obj - Object to which we will add a 'signatures' property
- */
- public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
- const sigs = new Map(Object.entries(obj.signatures || {}));
- const unsigned = obj.unsigned;
-
- delete obj.signatures;
- delete obj.unsigned;
-
- const userSignatures = sigs.get(this.userId) || {};
- sigs.set(this.userId, userSignatures);
- userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
- obj.signatures = recursiveMapToObject(sigs);
- if (unsigned !== undefined) obj.unsigned = unsigned;
- }
-}
-
-/**
- * Fix up the backup key, that may be in the wrong format due to a bug in a
- * migration step. Some backup keys were stored as a comma-separated list of
- * integers, rather than a base64-encoded byte array. If this function is
- * passed a string that looks like a list of integers rather than a base64
- * string, it will attempt to convert it to the right format.
- *
- * @param key - the key to check
- * @returns If the key is in the wrong format, then the fixed
- * key will be returned. Otherwise null will be returned.
- *
- */
-export function fixBackupKey(key?: string): string | null {
- if (typeof key !== "string" || key.indexOf(",") < 0) {
- return null;
- }
- const fixedKey = Uint8Array.from(key.split(","), (x) => parseInt(x));
- return olmlib.encodeBase64(fixedKey);
-}
-
-/**
- * Represents a received m.room_key_request event
- */
-export class IncomingRoomKeyRequest {
- /** user requesting the key */
- public readonly userId: string;
- /** device requesting the key */
- public readonly deviceId: string;
- /** unique id for the request */
- public readonly requestId: string;
- public readonly requestBody: IRoomKeyRequestBody;
- /**
- * callback which, when called, will ask
- * the relevant crypto algorithm implementation to share the keys for
- * this request.
- */
- public share: () => void;
-
- public constructor(event: MatrixEvent) {
- const content = event.getContent();
-
- this.userId = event.getSender()!;
- this.deviceId = content.requesting_device_id;
- this.requestId = content.request_id;
- this.requestBody = content.body || {};
- this.share = (): void => {
- throw new Error("don't know how to share keys for this request yet");
- };
- }
-}
-
-/**
- * Represents a received m.room_key_request cancellation
- */
-class IncomingRoomKeyRequestCancellation {
- /** user requesting the cancellation */
- public readonly userId: string;
- /** device requesting the cancellation */
- public readonly deviceId: string;
- /** unique id for the request to be cancelled */
- public readonly requestId: string;
-
- public constructor(event: MatrixEvent) {
- const content = event.getContent();
-
- this.userId = event.getSender()!;
- this.deviceId = content.requesting_device_id;
- this.requestId = content.request_id;
- }
-}
-
-// a number of types are re-exported for backwards compatibility, in case any applications are referencing it.
-export type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts
deleted file mode 100644
index f6fe7b6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
-Copyright 2018 - 2021 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 { randomString } from "../randomstring";
-import { subtleCrypto, TextEncoder } from "./crypto";
-
-const DEFAULT_ITERATIONS = 500000;
-
-const DEFAULT_BITSIZE = 256;
-
-/* eslint-disable camelcase */
-interface IAuthData {
- private_key_salt?: string;
- private_key_iterations?: number;
- private_key_bits?: number;
-}
-/* eslint-enable camelcase */
-
-interface IKey {
- key: Uint8Array;
- salt: string;
- iterations: number;
-}
-
-export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
- if (!global.Olm) {
- throw new Error("Olm is not available");
- }
-
- if (!authData.private_key_salt || !authData.private_key_iterations) {
- throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
- }
-
- return deriveKey(
- password,
- authData.private_key_salt,
- authData.private_key_iterations,
- authData.private_key_bits || DEFAULT_BITSIZE,
- );
-}
-
-export async function keyFromPassphrase(password: string): Promise<IKey> {
- if (!global.Olm) {
- throw new Error("Olm is not available");
- }
-
- const salt = randomString(32);
-
- const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
-
- return { key, salt, iterations: DEFAULT_ITERATIONS };
-}
-
-export async function deriveKey(
- password: string,
- salt: string,
- iterations: number,
- numBits = DEFAULT_BITSIZE,
-): Promise<Uint8Array> {
- if (!subtleCrypto || !TextEncoder) {
- throw new Error("Password-based backup is not available on this platform");
- }
-
- const key = await subtleCrypto.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, [
- "deriveBits",
- ]);
-
- const keybits = await subtleCrypto.deriveBits(
- {
- name: "PBKDF2",
- salt: new TextEncoder().encode(salt),
- iterations: iterations,
- hash: "SHA-512",
- },
- key,
- numBits,
- );
-
- return new Uint8Array(keybits);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts
deleted file mode 100644
index 67e213c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { ISigned } from "../@types/signed";
-import { IEncryptedPayload } from "./aes";
-
-export interface Curve25519SessionData {
- ciphertext: string;
- ephemeral: string;
- mac: string;
-}
-
-/* eslint-disable camelcase */
-export interface IKeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
- first_message_index: number;
- forwarded_count: number;
- is_verified: boolean;
- session_data: T;
-}
-
-export interface IKeyBackupRoomSessions {
- [sessionId: string]: IKeyBackupSession;
-}
-
-export interface ICurve25519AuthData {
- public_key: string;
- private_key_salt?: string;
- private_key_iterations?: number;
- private_key_bits?: number;
-}
-
-export interface IAes256AuthData {
- iv: string;
- mac: string;
- private_key_salt?: string;
- private_key_iterations?: number;
-}
-
-export interface IKeyBackupInfo {
- algorithm: string;
- auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
- count?: number;
- etag?: string;
- version?: string; // number contained within
-}
-/* eslint-enable camelcase */
-
-export interface IKeyBackupPrepareOpts {
- /**
- * Whether to use Secure Secret Storage to store the key encrypting key backups.
- * Optional, defaults to false.
- */
- secureSecretStorage: boolean;
-}
-
-export interface IKeyBackupRestoreResult {
- total: number;
- imported: number;
-}
-
-export interface IKeyBackupRestoreOpts {
- cacheCompleteCallback?: () => void;
- progressCallback?: (progress: { stage: string }) => void;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
deleted file mode 100644
index c37b7f0..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Utilities common to olm encryption algorithms
- */
-
-import anotherjson from "another-json";
-
-import type { PkSigning } from "@matrix-org/olm";
-import type { IOneTimeKey } from "../@types/crypto";
-import { OlmDevice } from "./OlmDevice";
-import { DeviceInfo } from "./deviceinfo";
-import { logger } from "../logger";
-import { IClaimOTKsResult, MatrixClient } from "../client";
-import { ISignatures } from "../@types/signed";
-import { MatrixEvent } from "../models/event";
-import { EventType } from "../@types/event";
-import { IMessage } from "./algorithms/olm";
-import { MapWithDefault } from "../utils";
-
-enum Algorithm {
- Olm = "m.olm.v1.curve25519-aes-sha2",
- Megolm = "m.megolm.v1.aes-sha2",
- MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
-}
-
-/**
- * matrix algorithm tag for olm
- */
-export const OLM_ALGORITHM = Algorithm.Olm;
-
-/**
- * matrix algorithm tag for megolm
- */
-export const MEGOLM_ALGORITHM = Algorithm.Megolm;
-
-/**
- * matrix algorithm tag for megolm backups
- */
-export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
-
-export interface IOlmSessionResult {
- /** device info */
- device: DeviceInfo;
- /** base64 olm session id; null if no session could be established */
- sessionId: string | null;
-}
-
-/**
- * Encrypt an event payload for an Olm device
- *
- * @param resultsObject - The `ciphertext` property
- * of the m.room.encrypted event to which to add our result
- *
- * @param olmDevice - olm.js wrapper
- * @param payloadFields - fields to include in the encrypted payload
- *
- * Returns a promise which resolves (to undefined) when the payload
- * has been encrypted into `resultsObject`
- */
-export async function encryptMessageForDevice(
- resultsObject: Record<string, IMessage>,
- ourUserId: string,
- ourDeviceId: string | undefined,
- olmDevice: OlmDevice,
- recipientUserId: string,
- recipientDevice: DeviceInfo,
- payloadFields: Record<string, any>,
-): Promise<void> {
- const deviceKey = recipientDevice.getIdentityKey();
- const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
- if (sessionId === null) {
- // If we don't have a session for a device then
- // we can't encrypt a message for it.
- logger.log(
- `[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` +
- `${recipientUserId}:${recipientDevice.deviceId}`,
- );
- return;
- }
-
- logger.log(
- `[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` +
- `${recipientUserId}:${recipientDevice.deviceId}`,
- );
-
- const payload = {
- sender: ourUserId,
- // TODO this appears to no longer be used whatsoever
- sender_device: ourDeviceId,
-
- // Include the Ed25519 key so that the recipient knows what
- // device this message came from.
- // We don't need to include the curve25519 key since the
- // recipient will already know this from the olm headers.
- // When combined with the device keys retrieved from the
- // homeserver signed by the ed25519 key this proves that
- // the curve25519 key and the ed25519 key are owned by
- // the same device.
- keys: {
- ed25519: olmDevice.deviceEd25519Key,
- },
-
- // include the recipient device details in the payload,
- // to avoid unknown key attacks, per
- // https://github.com/vector-im/vector-web/issues/2483
- recipient: recipientUserId,
- recipient_keys: {
- ed25519: recipientDevice.getFingerprint(),
- },
- ...payloadFields,
- };
-
- // TODO: technically, a bunch of that stuff only needs to be included for
- // pre-key messages: after that, both sides know exactly which devices are
- // involved in the session. If we're looking to reduce data transfer in the
- // future, we could elide them for subsequent messages.
-
- resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload));
-}
-
-interface IExistingOlmSession {
- device: DeviceInfo;
- sessionId: string | null;
-}
-
-/**
- * Get the existing olm sessions for the given devices, and the devices that
- * don't have olm sessions.
- *
- *
- *
- * @param devicesByUser - map from userid to list of devices to ensure sessions for
- *
- * @returns resolves to an array. The first element of the array is a
- * a map of user IDs to arrays of deviceInfo, representing the devices that
- * don't have established olm sessions. The second element of the array is
- * a map from userId to deviceId to {@link OlmSessionResult}
- */
-export async function getExistingOlmSessions(
- olmDevice: OlmDevice,
- baseApis: MatrixClient,
- devicesByUser: Record<string, DeviceInfo[]>,
-): Promise<[Map<string, DeviceInfo[]>, Map<string, Map<string, IExistingOlmSession>>]> {
- // map user Id → DeviceInfo[]
- const devicesWithoutSession: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
- // map user Id → device Id → IExistingOlmSession
- const sessions: MapWithDefault<string, Map<string, IExistingOlmSession>> = new MapWithDefault(() => new Map());
-
- const promises: Promise<void>[] = [];
-
- for (const [userId, devices] of Object.entries(devicesByUser)) {
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
- promises.push(
- (async (): Promise<void> => {
- const sessionId = await olmDevice.getSessionIdForDevice(key, true);
- if (sessionId === null) {
- devicesWithoutSession.getOrCreate(userId).push(deviceInfo);
- } else {
- sessions.getOrCreate(userId).set(deviceId, {
- device: deviceInfo,
- sessionId: sessionId,
- });
- }
- })(),
- );
- }
- }
-
- await Promise.all(promises);
-
- return [devicesWithoutSession, sessions];
-}
-
-/**
- * Try to make sure we have established olm sessions for the given devices.
- *
- * @param devicesByUser - map from userid to list of devices to ensure sessions for
- *
- * @param force - If true, establish a new session even if one
- * already exists.
- *
- * @param otkTimeout - The timeout in milliseconds when requesting
- * one-time keys for establishing new olm sessions.
- *
- * @param failedServers - An array to fill with remote servers that
- * failed to respond to one-time-key requests.
- *
- * @param log - A possibly customised log
- *
- * @returns resolves once the sessions are complete, to
- * an Object mapping from userId to deviceId to
- * {@link OlmSessionResult}
- */
-export async function ensureOlmSessionsForDevices(
- olmDevice: OlmDevice,
- baseApis: MatrixClient,
- devicesByUser: Map<string, DeviceInfo[]>,
- force = false,
- otkTimeout?: number,
- failedServers?: string[],
- log = logger,
-): Promise<Map<string, Map<string, IOlmSessionResult>>> {
- const devicesWithoutSession: [string, string][] = [
- // [userId, deviceId], ...
- ];
- // map user Id → device Id → IExistingOlmSession
- const result: Map<string, Map<string, IExistingOlmSession>> = new Map();
- // map device key → resolve session fn
- const resolveSession: Map<string, (sessionId?: string) => void> = new Map();
-
- // Mark all sessions this task intends to update as in progress. It is
- // important to do this for all devices this task cares about in a single
- // synchronous operation, as otherwise it is possible to have deadlocks
- // where multiple tasks wait indefinitely on another task to update some set
- // of common devices.
- for (const devices of devicesByUser.values()) {
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We don't start sessions with ourself, so there's no need to
- // mark it in progress.
- continue;
- }
-
- if (!olmDevice.sessionsInProgress[key]) {
- // pre-emptively mark the session as in-progress to avoid race
- // conditions. If we find that we already have a session, then
- // we'll resolve
- olmDevice.sessionsInProgress[key] = new Promise((resolve) => {
- resolveSession.set(key, (v: any): void => {
- delete olmDevice.sessionsInProgress[key];
- resolve(v);
- });
- });
- }
- }
- }
-
- for (const [userId, devices] of devicesByUser) {
- const resultDevices = new Map();
- result.set(userId, resultDevices);
-
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We should never be trying to start a session with ourself.
- // Apart from talking to yourself being the first sign of madness,
- // olm sessions can't do this because they get confused when
- // they get a message and see that the 'other side' has started a
- // new chain when this side has an active sender chain.
- // If you see this message being logged in the wild, we should find
- // the thing that is trying to send Olm messages to itself and fix it.
- log.info("Attempted to start session with ourself! Ignoring");
- // We must fill in the section in the return value though, as callers
- // expect it to be there.
- resultDevices.set(deviceId, {
- device: deviceInfo,
- sessionId: null,
- });
- continue;
- }
-
- const forWhom = `for ${key} (${userId}:${deviceId})`;
- const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log);
- const resolveSessionFn = resolveSession.get(key);
- if (sessionId !== null && resolveSessionFn) {
- // we found a session, but we had marked the session as
- // in-progress, so resolve it now, which will unmark it and
- // unblock anything that was waiting
- resolveSessionFn();
- }
- if (sessionId === null || force) {
- if (force) {
- log.info(`Forcing new Olm session ${forWhom}`);
- } else {
- log.info(`Making new Olm session ${forWhom}`);
- }
- devicesWithoutSession.push([userId, deviceId]);
- }
- resultDevices.set(deviceId, {
- device: deviceInfo,
- sessionId: sessionId,
- });
- }
- }
-
- if (devicesWithoutSession.length === 0) {
- return result;
- }
-
- const oneTimeKeyAlgorithm = "signed_curve25519";
- let res: IClaimOTKsResult;
- let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
- try {
- log.debug(`Claiming ${taskDetail}`);
- res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
- log.debug(`Claimed ${taskDetail}`);
- } catch (e) {
- for (const resolver of resolveSession.values()) {
- resolver();
- }
- log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
- throw e;
- }
-
- if (failedServers && "failures" in res) {
- failedServers.push(...Object.keys(res.failures));
- }
-
- const otkResult = res.one_time_keys || ({} as IClaimOTKsResult["one_time_keys"]);
- const promises: Promise<void>[] = [];
- for (const [userId, devices] of devicesByUser) {
- const userRes = otkResult[userId] || {};
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We've already logged about this above. Skip here too
- // otherwise we'll log saying there are no one-time keys
- // which will be confusing.
- continue;
- }
-
- if (result.get(userId)?.get(deviceId)?.sessionId && !force) {
- // we already have a result for this device
- continue;
- }
-
- const deviceRes = userRes[deviceId] || {};
- let oneTimeKey: IOneTimeKey | null = null;
- for (const keyId in deviceRes) {
- if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
- oneTimeKey = deviceRes[keyId];
- }
- }
-
- if (!oneTimeKey) {
- log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
- resolveSession.get(key)?.();
- continue;
- }
-
- promises.push(
- _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(
- (sid) => {
- resolveSession.get(key)?.(sid ?? undefined);
- const deviceInfo = result.get(userId)?.get(deviceId);
- if (deviceInfo) deviceInfo.sessionId = sid;
- },
- (e) => {
- resolveSession.get(key)?.();
- throw e;
- },
- ),
- );
- }
- }
-
- taskDetail = `Olm sessions for ${promises.length} devices`;
- log.debug(`Starting ${taskDetail}`);
- await Promise.all(promises);
- log.debug(`Started ${taskDetail}`);
- return result;
-}
-
-async function _verifyKeyAndStartSession(
- olmDevice: OlmDevice,
- oneTimeKey: IOneTimeKey,
- userId: string,
- deviceInfo: DeviceInfo,
-): Promise<string | null> {
- const deviceId = deviceInfo.deviceId;
- try {
- await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint());
- } catch (e) {
- logger.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e);
- return null;
- }
-
- let sid;
- try {
- sid = await olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key);
- } catch (e) {
- // possibly a bad key
- logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e);
- return null;
- }
-
- logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId);
- return sid;
-}
-
-export interface IObject {
- unsigned?: object;
- signatures?: ISignatures;
-}
-
-/**
- * Verify the signature on an object
- *
- * @param olmDevice - olm wrapper to use for verify op
- *
- * @param obj - object to check signature on.
- *
- * @param signingUserId - ID of the user whose signature should be checked
- *
- * @param signingDeviceId - ID of the device whose signature should be checked
- *
- * @param signingKey - base64-ed ed25519 public key
- *
- * Returns a promise which resolves (to undefined) if the the signature is good,
- * or rejects with an Error if it is bad.
- */
-export async function verifySignature(
- olmDevice: OlmDevice,
- obj: IOneTimeKey | IObject,
- signingUserId: string,
- signingDeviceId: string,
- signingKey: string,
-): Promise<void> {
- const signKeyId = "ed25519:" + signingDeviceId;
- const signatures = obj.signatures || {};
- const userSigs = signatures[signingUserId] || {};
- const signature = userSigs[signKeyId];
- if (!signature) {
- throw Error("No signature");
- }
-
- // prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson
- const mangledObj = Object.assign({}, obj);
- if ("unsigned" in mangledObj) {
- delete mangledObj.unsigned;
- }
- delete mangledObj.signatures;
- const json = anotherjson.stringify(mangledObj);
-
- olmDevice.verifySignature(signingKey, json, signature);
-}
-
-/**
- * Sign a JSON object using public key cryptography
- * @param obj - Object to sign. The object will be modified to include
- * the new signature
- * @param key - the signing object or the private key
- * seed
- * @param userId - The user ID who owns the signing key
- * @param pubKey - The public key (ignored if key is a seed)
- * @returns the signature for the object
- */
-export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
- let createdKey = false;
- if (key instanceof Uint8Array) {
- const keyObj = new global.Olm.PkSigning();
- pubKey = keyObj.init_with_seed(key);
- key = keyObj;
- createdKey = true;
- }
- const sigs = obj.signatures || {};
- delete obj.signatures;
- const unsigned = obj.unsigned;
- if (obj.unsigned) delete obj.unsigned;
- try {
- const mysigs = sigs[userId] || {};
- sigs[userId] = mysigs;
-
- return (mysigs["ed25519:" + pubKey] = key.sign(anotherjson.stringify(obj)));
- } finally {
- obj.signatures = sigs;
- if (unsigned) obj.unsigned = unsigned;
- if (createdKey) {
- key.free();
- }
- }
-}
-
-/**
- * Verify a signed JSON object
- * @param obj - Object to verify
- * @param pubKey - The public key to use to verify
- * @param userId - The user ID who signed the object
- */
-export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
- const keyId = "ed25519:" + pubKey;
- if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
- throw new Error("No signature");
- }
- const signature = obj.signatures[userId][keyId];
- const util = new global.Olm.Utility();
- const sigs = obj.signatures;
- delete obj.signatures;
- const unsigned = obj.unsigned;
- if (obj.unsigned) delete obj.unsigned;
- try {
- util.ed25519_verify(pubKey, anotherjson.stringify(obj), signature);
- } finally {
- obj.signatures = sigs;
- if (unsigned) obj.unsigned = unsigned;
- util.free();
- }
-}
-
-/**
- * Check that an event was encrypted using olm.
- */
-export function isOlmEncrypted(event: MatrixEvent): boolean {
- if (!event.getSenderKey()) {
- logger.error("Event has no sender key (not encrypted?)");
- return false;
- }
- if (
- event.getWireType() !== EventType.RoomMessageEncrypted ||
- !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)
- ) {
- logger.error("Event was not encrypted using an appropriate algorithm");
- return false;
- }
- return true;
-}
-
-/**
- * Encode a typed array of uint8 as base64.
- * @param uint8Array - The data to encode.
- * @returns The base64.
- */
-export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
- return Buffer.from(uint8Array).toString("base64");
-}
-
-/**
- * Encode a typed array of uint8 as unpadded base64.
- * @param uint8Array - The data to encode.
- * @returns The unpadded base64.
- */
-export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
- return encodeBase64(uint8Array).replace(/=+$/g, "");
-}
-
-/**
- * Decode a base64 string to a typed array of uint8.
- * @param base64 - The base64 to decode.
- * @returns The decoded data.
- */
-export function decodeBase64(base64: string): Uint8Array {
- return Buffer.from(base64, "base64");
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts
deleted file mode 100644
index 4107b76..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import * as bs58 from "bs58";
-
-// picked arbitrarily but to try & avoid clashing with any bitcoin ones
-// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
-const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
-
-export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
- const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
- buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
- buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
-
- let parity = 0;
- for (let i = 0; i < buf.length - 1; ++i) {
- parity ^= buf[i];
- }
- buf[buf.length - 1] = parity;
- const base58key = bs58.encode(buf);
-
- return base58key.match(/.{1,4}/g)?.join(" ");
-}
-
-export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
- const result = bs58.decode(recoveryKey.replace(/ /g, ""));
-
- let parity = 0;
- for (const b of result) {
- parity ^= b;
- }
- if (parity !== 0) {
- throw new Error("Incorrect parity");
- }
-
- for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
- if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
- throw new Error("Incorrect prefix");
- }
- }
-
- if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) {
- throw new Error("Incorrect length");
- }
-
- return Uint8Array.from(
- result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH),
- );
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts
deleted file mode 100644
index 4c88ec2..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
-Copyright 2021 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 { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
-import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { TrackingStatus } from "../DeviceList";
-import { IRoomEncryption } from "../RoomList";
-import { IDevice } from "../deviceinfo";
-import { ICrossSigningInfo } from "../CrossSigning";
-import { PrefixedLogger } from "../../logger";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { MatrixEvent } from "../../models/event";
-import { DehydrationManager } from "../dehydration";
-import { IEncryptedPayload } from "../aes";
-
-/**
- * Internal module. Definitions for storage for the crypto module
- */
-
-export interface SecretStorePrivateKeys {
- "dehydration": {
- keyInfo: DehydrationManager["keyInfo"];
- key: IEncryptedPayload;
- deviceDisplayName: string;
- time: number;
- } | null;
- "m.megolm_backup.v1": IEncryptedPayload;
-}
-
-/**
- * Abstraction of things that can store data required for end-to-end encryption
- */
-export interface CryptoStore {
- startup(): Promise<CryptoStore>;
- deleteAllData(): Promise<void>;
- getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
- getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
- getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
- getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
- getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]>;
- updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null>;
- deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
-
- // Olm Account
- getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
- storeAccount(txn: unknown, accountPickle: string): void;
- getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
- getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void;
- storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
- storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void;
-
- // Olm Sessions
- countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
- getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo | null) => void,
- ): void;
- getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void;
- getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
- storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
- storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
- getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
- filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
-
- // Inbound Group Sessions
- getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void;
- getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void;
- addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void;
- storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void;
- storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void;
-
- // Device Data
- getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
- storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
- storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
- getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
- getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
- countSessionsNeedingBackup(txn?: unknown): Promise<number>;
- unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
- markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
- addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
- getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: unknown,
- ): Promise<[senderKey: string, sessionId: string][]>;
- addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
- takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
-
- // Session key backups
- doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
-}
-
-export type Mode = "readonly" | "readwrite";
-
-export interface ISession {
- senderKey: string;
- sessionId: string;
- sessionData?: InboundGroupSessionData;
-}
-
-export interface ISessionInfo {
- deviceKey?: string;
- sessionId?: string;
- session?: string;
- lastReceivedMessageTs?: number;
-}
-
-export interface IDeviceData {
- devices: {
- [userId: string]: {
- [deviceId: string]: IDevice;
- };
- };
- trackingStatus: {
- [userId: string]: TrackingStatus;
- };
- crossSigningInfo?: Record<string, ICrossSigningInfo>;
- syncToken?: string;
-}
-
-export interface IProblem {
- type: string;
- fixed: boolean;
- time: number;
-}
-
-export interface IWithheld {
- // eslint-disable-next-line camelcase
- room_id: string;
- code: string;
- reason: string;
-}
-
-/**
- * Represents an outgoing room key request
- */
-export interface OutgoingRoomKeyRequest {
- /**
- * Unique id for this request. Used for both an id within the request for later pairing with a cancellation,
- * and for the transaction id when sending the to_device messages to our local server.
- */
- requestId: string;
- requestTxnId?: string;
- /**
- * Transaction id for the cancellation, if any
- */
- cancellationTxnId?: string;
- /**
- * List of recipients for the request
- */
- recipients: IRoomKeyRequestRecipient[];
- /**
- * Parameters for the request
- */
- requestBody: IRoomKeyRequestBody;
- /**
- * current state of this request (states are defined in {@link OutgoingRoomKeyRequestManager})
- */
- state: RoomKeyRequestState;
-}
-
-export interface ParkedSharedHistory {
- senderId: string;
- senderKey: string;
- sessionId: string;
- sessionKey: string;
- keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
- forwardingCurve25519KeyChain: string[];
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
deleted file mode 100644
index 7827697..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
+++ /dev/null
@@ -1,1062 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { logger, PrefixedLogger } from "../../logger";
-import * as utils from "../../utils";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-
-const PROFILE_TRANSACTIONS = false;
-
-/**
- * Implementation of a CryptoStore which is backed by an existing
- * IndexedDB connection. Generally you want IndexedDBCryptoStore
- * which connects to the database and defers to one of these.
- */
-export class Backend implements CryptoStore {
- private nextTxnId = 0;
-
- /**
- */
- public constructor(private db: IDBDatabase) {
- // make sure we close the db on `onversionchange` - otherwise
- // attempts to delete the database will block (and subsequent
- // attempts to re-create it will also block).
- db.onversionchange = (): void => {
- logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
- db.close();
- };
- }
-
- public async startup(): Promise<CryptoStore> {
- // No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
- // by passing us a ready IDBDatabase instance
- return this;
- }
- public async deleteAllData(): Promise<void> {
- throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- const requestBody = request.requestBody;
-
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- txn.onerror = reject;
-
- // first see if we already have an entry for this request.
- this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
- if (existing) {
- // this entry matches the request - return it.
- logger.log(
- `already have key request outstanding for ` +
- `${requestBody.room_id} / ${requestBody.session_id}: ` +
- `not sending another`,
- );
- resolve(existing);
- return;
- }
-
- // we got to the end of the list without finding a match
- // - add the new request.
- logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
- txn.oncomplete = (): void => {
- resolve(request);
- };
- const store = txn.objectStore("outgoingRoomKeyRequests");
- store.add(request);
- });
- });
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- txn.onerror = reject;
-
- this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
- resolve(existing);
- });
- });
- }
-
- /**
- * look for an existing room key request in the db
- *
- * @internal
- * @param txn - database transaction
- * @param requestBody - existing request to look for
- * @param callback - function to call with the results of the
- * search. Either passed a matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found.
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getOutgoingRoomKeyRequest(
- txn: IDBTransaction,
- requestBody: IRoomKeyRequestBody,
- callback: (req: OutgoingRoomKeyRequest | null) => void,
- ): void {
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const idx = store.index("session");
- const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);
-
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- // no match found
- callback(null);
- return;
- }
-
- const existing = cursor.value;
-
- if (utils.deepCompare(existing.requestBody, requestBody)) {
- // got a match
- callback(existing);
- return;
- }
-
- // look at the next entry in the index
- cursor.continue();
- };
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states. If there are multiple
- * requests in those states, an arbitrary one is chosen.
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- if (wantedStates.length === 0) {
- return Promise.resolve(null);
- }
-
- // this is a bit tortuous because we need to make sure we do the lookup
- // in a single transaction, to avoid having a race with the insertion
- // code.
-
- // index into the wantedStates array
- let stateIndex = 0;
- let result: OutgoingRoomKeyRequest;
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (cursor) {
- // got a match
- result = cursor.value;
- return;
- }
-
- // try the next state in the list
- stateIndex++;
- if (stateIndex >= wantedStates.length) {
- // no matches
- return;
- }
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = store.index("state").openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
-
- return promiseifyTxn(txn).then(() => result);
- }
-
- /**
- *
- * @returns All elements in a given state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
- const index = store.index("state");
- const request = index.getAll(wantedState);
-
- request.onsuccess = (): void => resolve(request.result);
- request.onerror = (): void => reject(request.error);
- });
- }
-
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- let stateIndex = 0;
- const results: OutgoingRoomKeyRequest[] = [];
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (cursor) {
- const keyReq = cursor.value;
- if (
- keyReq.recipients.some(
- (recipient: IRoomKeyRequestRecipient) =>
- recipient.userId === userId && recipient.deviceId === deviceId,
- )
- ) {
- results.push(keyReq);
- }
- cursor.continue();
- } else {
- // try the next state in the list
- stateIndex++;
- if (stateIndex >= wantedStates.length) {
- // no matches
- return;
- }
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
- }
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = store.index("state").openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
-
- return promiseifyTxn(txn).then(() => results);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- let result: OutgoingRoomKeyRequest | null = null;
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (!cursor) {
- return;
- }
- const data = cursor.value;
- if (data.state != expectedState) {
- logger.warn(
- `Cannot update room key request from ${expectedState} ` +
- `as it was already updated to ${data.state}`,
- );
- return;
- }
- Object.assign(data, updates);
- cursor.update(data);
- result = data;
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
- cursorReq.onsuccess = onsuccess;
- return promiseifyTxn(txn).then(() => result);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- return;
- }
- const data = cursor.value;
- if (data.state != expectedState) {
- logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`);
- return;
- }
- cursor.delete();
- };
- return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
- }
-
- // Olm Account
-
- public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get("-");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeAccount(txn: IDBTransaction, accountPickle: string): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(accountPickle, "-");
- }
-
- public getCrossSigningKeys(
- txn: IDBTransaction,
- func: (keys: Record<string, ICrossSigningKey> | null) => void,
- ): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get("crossSigningKeys");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get(`ssss_cache:${type}`);
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(keys, "crossSigningKeys");
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(key, `ssss_cache:${type}`);
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
- const objectStore = txn.objectStore("sessions");
- const countReq = objectStore.count();
- countReq.onsuccess = function (): void {
- try {
- func(countReq.result);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: IDBTransaction,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- const objectStore = txn.objectStore("sessions");
- const idx = objectStore.index("deviceKey");
- const getReq = idx.openCursor(deviceKey);
- const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- results[cursor.value.sessionId] = {
- session: cursor.value.session,
- lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
- };
- cursor.continue();
- } else {
- try {
- func(results);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (session: ISessionInfo | null) => void,
- ): void {
- const objectStore = txn.objectStore("sessions");
- const getReq = objectStore.get([deviceKey, sessionId]);
- getReq.onsuccess = function (): void {
- try {
- if (getReq.result) {
- func({
- session: getReq.result.session,
- lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
- });
- } else {
- func(null);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
- const objectStore = txn.objectStore("sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- try {
- const cursor = getReq.result;
- if (cursor) {
- func(cursor.value);
- cursor.continue();
- } else {
- func(null);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeEndToEndSession(
- deviceKey: string,
- sessionId: string,
- sessionInfo: ISessionInfo,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("sessions");
- objectStore.put({
- deviceKey,
- sessionId,
- session: sessionInfo.session,
- lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
- });
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const txn = this.db.transaction("session_problems", "readwrite");
- const objectStore = txn.objectStore("session_problems");
- objectStore.put({
- deviceKey,
- type,
- fixed,
- time: Date.now(),
- });
- await promiseifyTxn(txn);
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- let result: IProblem | null = null;
- const txn = this.db.transaction("session_problems", "readwrite");
- const objectStore = txn.objectStore("session_problems");
- const index = objectStore.index("deviceKey");
- const req = index.getAll(deviceKey);
- req.onsuccess = (): void => {
- const problems = req.result;
- if (!problems.length) {
- result = null;
- return;
- }
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- result = Object.assign({}, problem, { fixed: lastProblem.fixed });
- return;
- }
- }
- if (lastProblem.fixed) {
- result = null;
- } else {
- result = lastProblem;
- }
- };
- await promiseifyTxn(txn);
- return result;
- }
-
- // FIXME: we should probably prune this when devices get deleted
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const txn = this.db.transaction("notified_error_devices", "readwrite");
- const objectStore = txn.objectStore("notified_error_devices");
-
- const ret: IOlmDevice[] = [];
-
- await Promise.all(
- devices.map((device) => {
- return new Promise<void>((resolve) => {
- const { userId, deviceInfo } = device;
- const getReq = objectStore.get([userId, deviceInfo.deviceId]);
- getReq.onsuccess = function (): void {
- if (!getReq.result) {
- objectStore.put({ userId, deviceId: deviceInfo.deviceId });
- ret.push(device);
- }
- resolve();
- };
- });
- }),
- );
-
- return ret;
- }
-
- // Inbound group sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- let session: InboundGroupSessionData | null | boolean = false;
- let withheld: IWithheld | null | boolean = false;
- const objectStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.get([senderCurve25519Key, sessionId]);
- getReq.onsuccess = function (): void {
- try {
- if (getReq.result) {
- session = getReq.result.session;
- } else {
- session = null;
- }
- if (withheld !== false) {
- func(session as InboundGroupSessionData, withheld as IWithheld);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
-
- const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
- const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
- withheldGetReq.onsuccess = function (): void {
- try {
- if (withheldGetReq.result) {
- withheld = withheldGetReq.result.session;
- } else {
- withheld = null;
- }
- if (session !== false) {
- func(session as InboundGroupSessionData, withheld as IWithheld);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- try {
- func({
- senderKey: cursor.value.senderCurve25519Key,
- sessionId: cursor.value.sessionId,
- sessionData: cursor.value.session,
- });
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- cursor.continue();
- } else {
- try {
- func(null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- const addReq = objectStore.add({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- addReq.onerror = (ev): void => {
- if (addReq.error?.name === "ConstraintError") {
- // This stops the error from triggering the txn's onerror
- ev.stopPropagation();
- // ...and this stops it from aborting the transaction
- ev.preventDefault();
- logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
- } else {
- abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
- }
- };
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- objectStore.put({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions_withheld");
- objectStore.put({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- }
-
- public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
- const objectStore = txn.objectStore("device_data");
- const getReq = objectStore.get("-");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
- const objectStore = txn.objectStore("device_data");
- objectStore.put(deviceData, "-");
- }
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
- const objectStore = txn.objectStore("rooms");
- objectStore.put(roomInfo, roomId);
- }
-
- public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
- const objectStore = txn.objectStore("rooms");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- rooms[cursor.key as string] = cursor.value;
- cursor.continue();
- } else {
- try {
- func(rooms);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- // session backups
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- return new Promise((resolve, reject) => {
- const sessions: ISession[] = [];
-
- const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
- txn.onerror = reject;
- txn.oncomplete = function (): void {
- resolve(sessions);
- };
- const objectStore = txn.objectStore("sessions_needing_backup");
- const sessionStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- const sessionGetReq = sessionStore.get(cursor.key);
- sessionGetReq.onsuccess = function (): void {
- sessions.push({
- senderKey: sessionGetReq.result.senderCurve25519Key,
- sessionId: sessionGetReq.result.sessionId,
- sessionData: sessionGetReq.result.session,
- });
- };
- if (!limit || sessions.length < limit) {
- cursor.continue();
- }
- }
- };
- });
- }
-
- public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readonly");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- return new Promise((resolve, reject) => {
- const req = objectStore.count();
- req.onerror = reject;
- req.onsuccess = (): void => resolve(req.result);
- });
- }
-
- public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readwrite");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- await Promise.all(
- sessions.map((session) => {
- return new Promise((resolve, reject) => {
- const req = objectStore.delete([session.senderKey, session.sessionId]);
- req.onsuccess = resolve;
- req.onerror = reject;
- });
- }),
- );
- }
-
- public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readwrite");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- await Promise.all(
- sessions.map((session) => {
- return new Promise((resolve, reject) => {
- const req = objectStore.put({
- senderCurve25519Key: session.senderKey,
- sessionId: session.sessionId,
- });
- req.onsuccess = resolve;
- req.onerror = reject;
- });
- }),
- );
- }
-
- public addSharedHistoryInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn?: IDBTransaction,
- ): void {
- if (!txn) {
- txn = this.db.transaction("shared_history_inbound_group_sessions", "readwrite");
- }
- const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
- const req = objectStore.get([roomId]);
- req.onsuccess = (): void => {
- const { sessions } = req.result || { sessions: [] };
- sessions.push([senderKey, sessionId]);
- objectStore.put({ roomId, sessions });
- };
- }
-
- public getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: IDBTransaction,
- ): Promise<[senderKey: string, sessionId: string][]> {
- if (!txn) {
- txn = this.db.transaction("shared_history_inbound_group_sessions", "readonly");
- }
- const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
- const req = objectStore.get([roomId]);
- return new Promise((resolve, reject) => {
- req.onsuccess = (): void => {
- const { sessions } = req.result || { sessions: [] };
- resolve(sessions);
- };
- req.onerror = reject;
- });
- }
-
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
- if (!txn) {
- txn = this.db.transaction("parked_shared_history", "readwrite");
- }
- const objectStore = txn.objectStore("parked_shared_history");
- const req = objectStore.get([roomId]);
- req.onsuccess = (): void => {
- const { parked } = req.result || { parked: [] };
- parked.push(parkedData);
- objectStore.put({ roomId, parked });
- };
- }
-
- public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
- if (!txn) {
- txn = this.db.transaction("parked_shared_history", "readwrite");
- }
- const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
- return new Promise((resolve, reject) => {
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- resolve([]);
- return;
- }
- const data = cursor.value;
- cursor.delete();
- resolve(data);
- };
- cursorReq.onerror = reject;
- });
- }
-
- public doTxn<T>(
- mode: Mode,
- stores: string | string[],
- func: (txn: IDBTransaction) => T,
- log: PrefixedLogger = logger,
- ): Promise<T> {
- let startTime: number;
- let description: string;
- if (PROFILE_TRANSACTIONS) {
- const txnId = this.nextTxnId++;
- startTime = Date.now();
- description = `${mode} crypto store transaction ${txnId} in ${stores}`;
- log.debug(`Starting ${description}`);
- }
- const txn = this.db.transaction(stores, mode);
- const promise = promiseifyTxn(txn);
- const result = func(txn);
- if (PROFILE_TRANSACTIONS) {
- promise.then(
- () => {
- const elapsedTime = Date.now() - startTime;
- log.debug(`Finished ${description}, took ${elapsedTime} ms`);
- },
- () => {
- const elapsedTime = Date.now() - startTime;
- log.error(`Failed ${description}, took ${elapsedTime} ms`);
- },
- );
- }
- return promise.then(() => {
- return result;
- });
- }
-}
-
-type DbMigration = (db: IDBDatabase) => void;
-const DB_MIGRATIONS: DbMigration[] = [
- (db): void => {
- createDatabase(db);
- },
- (db): void => {
- db.createObjectStore("account");
- },
- (db): void => {
- const sessionsStore = db.createObjectStore("sessions", {
- keyPath: ["deviceKey", "sessionId"],
- });
- sessionsStore.createIndex("deviceKey", "deviceKey");
- },
- (db): void => {
- db.createObjectStore("inbound_group_sessions", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- db.createObjectStore("device_data");
- },
- (db): void => {
- db.createObjectStore("rooms");
- },
- (db): void => {
- db.createObjectStore("sessions_needing_backup", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- db.createObjectStore("inbound_group_sessions_withheld", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- const problemsStore = db.createObjectStore("session_problems", {
- keyPath: ["deviceKey", "time"],
- });
- problemsStore.createIndex("deviceKey", "deviceKey");
-
- db.createObjectStore("notified_error_devices", {
- keyPath: ["userId", "deviceId"],
- });
- },
- (db): void => {
- db.createObjectStore("shared_history_inbound_group_sessions", {
- keyPath: ["roomId"],
- });
- },
- (db): void => {
- db.createObjectStore("parked_shared_history", {
- keyPath: ["roomId"],
- });
- },
- // Expand as needed.
-];
-export const VERSION = DB_MIGRATIONS.length;
-
-export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
- logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`);
- DB_MIGRATIONS.forEach((migration, index) => {
- if (oldVersion <= index) migration(db);
- });
-}
-
-function createDatabase(db: IDBDatabase): void {
- const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
-
- // we assume that the RoomKeyRequestBody will have room_id and session_id
- // properties, to make the index efficient.
- outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);
-
- outgoingRoomKeyRequestsStore.createIndex("state", "state");
-}
-
-interface IWrappedIDBTransaction extends IDBTransaction {
- _mx_abortexception: Error; // eslint-disable-line camelcase
-}
-
-/*
- * Aborts a transaction with a given exception
- * The transaction promise will be rejected with this exception.
- */
-function abortWithException(txn: IDBTransaction, e: Error): void {
- // We cheekily stick our exception onto the transaction object here
- // We could alternatively make the thing we pass back to the app
- // an object containing the transaction and exception.
- (txn as IWrappedIDBTransaction)._mx_abortexception = e;
- try {
- txn.abort();
- } catch (e) {
- // sometimes we won't be able to abort the transaction
- // (ie. if it's aborted or completed)
- }
-}
-
-function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
- return new Promise((resolve, reject) => {
- txn.oncomplete = (): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- }
- resolve(null);
- };
- txn.onerror = (event): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- } else {
- logger.log("Error performing indexeddb txn", event);
- reject(txn.error);
- }
- };
- txn.onabort = (event): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- } else {
- logger.log("Error performing indexeddb txn", event);
- reject(txn.error);
- }
- };
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
deleted file mode 100644
index 320235f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
+++ /dev/null
@@ -1,708 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { logger, PrefixedLogger } from "../../logger";
-import { LocalStorageCryptoStore } from "./localStorage-crypto-store";
-import { MemoryCryptoStore } from "./memory-crypto-store";
-import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend";
-import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors";
-import * as IndexedDBHelpers from "../../indexeddb-helpers";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-
-/**
- * Internal module. indexeddb storage for e2e.
- */
-
-/**
- * An implementation of CryptoStore, which is normally backed by an indexeddb,
- * but with fallback to MemoryCryptoStore.
- */
-export class IndexedDBCryptoStore implements CryptoStore {
- public static STORE_ACCOUNT = "account";
- public static STORE_SESSIONS = "sessions";
- public static STORE_INBOUND_GROUP_SESSIONS = "inbound_group_sessions";
- public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = "inbound_group_sessions_withheld";
- public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = "shared_history_inbound_group_sessions";
- public static STORE_PARKED_SHARED_HISTORY = "parked_shared_history";
- public static STORE_DEVICE_DATA = "device_data";
- public static STORE_ROOMS = "rooms";
- public static STORE_BACKUP = "sessions_needing_backup";
-
- public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
- return IndexedDBHelpers.exists(indexedDB, dbName);
- }
-
- private backendPromise?: Promise<CryptoStore>;
- private backend?: CryptoStore;
-
- /**
- * Create a new IndexedDBCryptoStore
- *
- * @param indexedDB - global indexedDB instance
- * @param dbName - name of db to connect to
- */
- public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
-
- /**
- * Ensure the database exists and is up-to-date, or fall back to
- * a local storage or in-memory store.
- *
- * This must be called before the store can be used.
- *
- * @returns resolves to either an IndexedDBCryptoStoreBackend.Backend,
- * or a MemoryCryptoStore
- */
- public startup(): Promise<CryptoStore> {
- if (this.backendPromise) {
- return this.backendPromise;
- }
-
- this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
- if (!this.indexedDB) {
- reject(new Error("no indexeddb support available"));
- return;
- }
-
- logger.log(`connecting to indexeddb ${this.dbName}`);
-
- const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
-
- req.onupgradeneeded = (ev): void => {
- const db = req.result;
- const oldVersion = ev.oldVersion;
- IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
- };
-
- req.onblocked = (): void => {
- logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`);
- };
-
- req.onerror = (ev): void => {
- logger.log("Error connecting to indexeddb", ev);
- reject(req.error);
- };
-
- req.onsuccess = (): void => {
- const db = req.result;
-
- logger.log(`connected to indexeddb ${this.dbName}`);
- resolve(new IndexedDBCryptoStoreBackend.Backend(db));
- };
- })
- .then((backend) => {
- // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
- // Try a dummy query which will fail if the browser doesn't support compund keys, so
- // we can fall back to a different backend.
- return backend
- .doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- backend.getEndToEndInboundGroupSession("", "", txn, () => {});
- },
- )
- .then(() => backend);
- })
- .catch((e) => {
- if (e.name === "VersionError") {
- logger.warn("Crypto DB is too new for us to use!", e);
- // don't fall back to a different store: the user has crypto data
- // in this db so we should use it or nothing at all.
- throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
- }
- logger.warn(
- `unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`,
- );
-
- try {
- return new LocalStorageCryptoStore(global.localStorage);
- } catch (e) {
- logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
- return new MemoryCryptoStore();
- }
- })
- .then((backend) => {
- this.backend = backend;
- return backend;
- });
-
- return this.backendPromise;
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- if (!this.indexedDB) {
- reject(new Error("no indexeddb support available"));
- return;
- }
-
- logger.log(`Removing indexeddb instance: ${this.dbName}`);
- const req = this.indexedDB.deleteDatabase(this.dbName);
-
- req.onblocked = (): void => {
- logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`);
- };
-
- req.onerror = (ev): void => {
- logger.log("Error deleting data from indexeddb", ev);
- reject(req.error);
- };
-
- req.onsuccess = (): void => {
- logger.log(`Removed indexeddb instance: ${this.dbName}`);
- resolve();
- };
- }).catch((e) => {
- // in firefox, with indexedDB disabled, this fails with a
- // DOMError. We treat this as non-fatal, so that people can
- // still use the app.
- logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
- });
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.getOutgoingRoomKeyRequest(requestBody);
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states. If there are multiple
- * requests in those states, an arbitrary one is chosen.
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
- }
-
- /**
- * Look for room key requests by state –
- * unlike above, return a list of all entries in one state.
- *
- * @returns Returns an array of requests in the given state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
- }
-
- /**
- * Look for room key requests by target device and state
- *
- * @param userId - Target user ID
- * @param deviceId - Target device ID
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to a list of all the
- * {@link OutgoingRoomKeyRequest}
- */
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- return this.backend!.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.updateOutgoingRoomKeyRequest(requestId, expectedState, updates);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
- }
-
- // Olm Account
-
- /*
- * Get the account pickle from the store.
- * This requires an active transaction. See doTxn().
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the account pickle
- */
- public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
- this.backend!.getAccount(txn, func);
- }
-
- /**
- * Write the account pickle to the store.
- * This requires an active transaction. See doTxn().
- *
- * @param txn - An active transaction. See doTxn().
- * @param accountPickle - The new account pickle to store.
- */
- public storeAccount(txn: IDBTransaction, accountPickle: string): void {
- this.backend!.storeAccount(txn, accountPickle);
- }
-
- /**
- * Get the public part of the cross-signing keys (eg. self-signing key,
- * user signing key).
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the account keys object:
- * `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed
- */
- public getCrossSigningKeys(
- txn: IDBTransaction,
- func: (keys: Record<string, ICrossSigningKey> | null) => void,
- ): void {
- this.backend!.getCrossSigningKeys(txn, func);
- }
-
- /**
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the private key
- * @param type - A key type
- */
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- this.backend!.getSecretStorePrivateKey(txn, func, type);
- }
-
- /**
- * Write the cross-signing keys back to the store
- *
- * @param txn - An active transaction. See doTxn().
- * @param keys - keys object as getCrossSigningKeys()
- */
- public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
- this.backend!.storeCrossSigningKeys(txn, keys);
- }
-
- /**
- * Write the cross-signing private keys back to the store
- *
- * @param txn - An active transaction. See doTxn().
- * @param type - The type of cross-signing private key to store
- * @param key - keys object as getCrossSigningKeys()
- */
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- this.backend!.storeSecretStorePrivateKey(txn, type, key);
- }
-
- // Olm sessions
-
- /**
- * Returns the number of end-to-end sessions in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the count of sessions
- */
- public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
- this.backend!.countEndToEndSessions(txn, func);
- }
-
- /**
- * Retrieve a specific end-to-end session between the logged-in user
- * and another device.
- * @param deviceKey - The public key of the other device.
- * @param sessionId - The ID of the session to retrieve
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to session information object with 'session' key being the
- * Base64 end-to-end session and lastReceivedMessageTs being the
- * timestamp in milliseconds at which the session last received
- * a message.
- */
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (session: ISessionInfo | null) => void,
- ): void {
- this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func);
- }
-
- /**
- * Retrieve the end-to-end sessions between the logged-in user and another
- * device.
- * @param deviceKey - The public key of the other device.
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to session information object with 'session' key being the
- * Base64 end-to-end session and lastReceivedMessageTs being the
- * timestamp in milliseconds at which the session last received
- * a message.
- */
- public getEndToEndSessions(
- deviceKey: string,
- txn: IDBTransaction,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- this.backend!.getEndToEndSessions(deviceKey, txn, func);
- }
-
- /**
- * Retrieve all end-to-end sessions
- * @param txn - An active transaction. See doTxn().
- * @param func - Called one for each session with
- * an object with, deviceKey, lastReceivedMessageTs, sessionId
- * and session keys.
- */
- public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
- this.backend!.getAllEndToEndSessions(txn, func);
- }
-
- /**
- * Store a session between the logged-in user and another device
- * @param deviceKey - The public key of the other device.
- * @param sessionId - The ID for this end-to-end session.
- * @param sessionInfo - Session information object
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndSession(
- deviceKey: string,
- sessionId: string,
- sessionInfo: ISessionInfo,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
- }
-
- public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
- }
-
- public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
- }
-
- public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- return this.backend!.filterOutNotifiedErrorDevices(devices);
- }
-
- // Inbound group sessions
-
- /**
- * Retrieve the end-to-end inbound group session for a given
- * server key and session ID
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to Base64 end-to-end session.
- */
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
- }
-
- /**
- * Fetches all inbound group sessions in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Called once for each group session
- * in the store with an object having keys `{senderKey, sessionId, sessionData}`,
- * then once with null to indicate the end of the list.
- */
- public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
- this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
- }
-
- /**
- * Adds an end-to-end inbound group session to the store.
- * If there already exists an inbound group session with the same
- * senderCurve25519Key and sessionID, the session will not be added.
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param sessionData - The session data structure
- * @param txn - An active transaction. See doTxn().
- */
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- /**
- * Writes an end-to-end inbound group session to the store.
- * If there already exists an inbound group session with the same
- * senderCurve25519Key and sessionID, it will be overwritten.
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param sessionData - The session data structure
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- // End-to-end device tracking
-
- /**
- * Store the state of all tracked devices
- * This contains devices for each user, a tracking state for each user
- * and a sync token matching the point in time the snapshot represents.
- * These all need to be written out in full each time such that the snapshot
- * is always consistent, so they are stored in one object.
- *
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
- this.backend!.storeEndToEndDeviceData(deviceData, txn);
- }
-
- /**
- * Get the state of all tracked devices
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Function called with the
- * device data
- */
- public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
- this.backend!.getEndToEndDeviceData(txn, func);
- }
-
- // End to End Rooms
-
- /**
- * Store the end-to-end state for a room.
- * @param roomId - The room's ID.
- * @param roomInfo - The end-to-end info for the room.
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
- this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
- }
-
- /**
- * Get an object of `roomId->roomInfo` for all e2e rooms in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Function called with the end-to-end encrypted rooms
- */
- public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- this.backend!.getEndToEndRooms(txn, func);
- }
-
- // session backups
-
- /**
- * Get the inbound group sessions that need to be backed up.
- * @param limit - The maximum number of sessions to retrieve. 0
- * for no limit.
- * @returns resolves to an array of inbound group sessions
- */
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- return this.backend!.getSessionsNeedingBackup(limit);
- }
-
- /**
- * Count the inbound group sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves to the number of sessions
- */
- public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
- return this.backend!.countSessionsNeedingBackup(txn);
- }
-
- /**
- * Unmark sessions as needing to be backed up.
- * @param sessions - The sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves when the sessions are unmarked
- */
- public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
- }
-
- /**
- * Mark sessions as needing to be backed up.
- * @param sessions - The sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves when the sessions are marked
- */
- public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- return this.backend!.markSessionsNeedingBackup(sessions, txn);
- }
-
- /**
- * Add a shared-history group session for a room.
- * @param roomId - The room that the key belongs to
- * @param senderKey - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param txn - An active transaction. See doTxn(). (optional)
- */
- public addSharedHistoryInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn?: IDBTransaction,
- ): void {
- this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
- }
-
- /**
- * Get the shared-history group session for a room.
- * @param roomId - The room that the key belongs to
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns Promise which resolves to an array of [senderKey, sessionId]
- */
- public getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: IDBTransaction,
- ): Promise<[senderKey: string, sessionId: string][]> {
- return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
- }
-
- /**
- * Park a shared-history group session for a room we may be invited to later.
- */
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
- this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
- }
-
- /**
- * Pop out all shared-history group sessions for a room.
- */
- public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
- return this.backend!.takeParkedSharedHistory(roomId, txn);
- }
-
- /**
- * Perform a transaction on the crypto store. Any store methods
- * that require a transaction (txn) object to be passed in may
- * only be called within a callback of either this function or
- * one of the store functions operating on the same transaction.
- *
- * @param mode - 'readwrite' if you need to call setter
- * functions with this transaction. Otherwise, 'readonly'.
- * @param stores - List IndexedDBCryptoStore.STORE_*
- * options representing all types of object that will be
- * accessed or written to with this transaction.
- * @param func - Function called with the
- * transaction object: an opaque object that should be passed
- * to store functions.
- * @param log - A possibly customised log
- * @returns Promise that resolves with the result of the `func`
- * when the transaction is complete. If the backend is
- * async (ie. the indexeddb backend) any of the callback
- * functions throwing an exception will cause this promise to
- * reject with that exception. On synchronous backends, the
- * exception will propagate to the caller of the getFoo method.
- */
- public doTxn<T>(
- mode: Mode,
- stores: Iterable<string>,
- func: (txn: IDBTransaction) => T,
- log?: PrefixedLogger,
- ): Promise<T> {
- return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
deleted file mode 100644
index 5552540..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
+++ /dev/null
@@ -1,403 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { logger } from "../../logger";
-import { MemoryCryptoStore } from "./memory-crypto-store";
-import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { ICrossSigningKey } from "../../client";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { safeSet } from "../../utils";
-
-/**
- * Internal module. Partial localStorage backed storage for e2e.
- * This is not a full crypto store, just the in-memory store with
- * some things backed by localStorage. It exists because indexedDB
- * is broken in Firefox private mode or set to, "will not remember
- * history".
- */
-
-const E2E_PREFIX = "crypto.";
-const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
-const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
-const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
-const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
-const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
-const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
-const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
-const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
-
-function keyEndToEndSessions(deviceKey: string): string {
- return E2E_PREFIX + "sessions/" + deviceKey;
-}
-
-function keyEndToEndSessionProblems(deviceKey: string): string {
- return E2E_PREFIX + "session.problems/" + deviceKey;
-}
-
-function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
- return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
-}
-
-function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
- return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
-}
-
-function keyEndToEndRoomsPrefix(roomId: string): string {
- return KEY_ROOMS_PREFIX + roomId;
-}
-
-export class LocalStorageCryptoStore extends MemoryCryptoStore {
- public static exists(store: Storage): boolean {
- const length = store.length;
- for (let i = 0; i < length; i++) {
- if (store.key(i)?.startsWith(E2E_PREFIX)) {
- return true;
- }
- }
- return false;
- }
-
- public constructor(private readonly store: Storage) {
- super();
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
- let count = 0;
- for (let i = 0; i < this.store.length; ++i) {
- if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) ++count;
- }
- func(count);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
- const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
- const fixedSessions: Record<string, ISessionInfo> = {};
-
- // fix up any old sessions to be objects rather than just the base64 pickle
- for (const [sid, val] of Object.entries(sessions || {})) {
- if (typeof val === "string") {
- fixedSessions[sid] = {
- session: val,
- };
- } else {
- fixedSessions[sid] = val;
- }
- }
-
- return fixedSessions;
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo) => void,
- ): void {
- const sessions = this._getEndToEndSessions(deviceKey);
- func(sessions[sessionId] || {});
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- func(this._getEndToEndSessions(deviceKey) || {});
- }
-
- public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
- for (let i = 0; i < this.store.length; ++i) {
- if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
- const deviceKey = this.store.key(i)!.split("/")[1];
- for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
- func(sess);
- }
- }
- }
- }
-
- public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
- const sessions = this._getEndToEndSessions(deviceKey) || {};
- sessions[sessionId] = sessionInfo;
- setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const key = keyEndToEndSessionProblems(deviceKey);
- const problems = getJsonItem<IProblem[]>(this.store, key) || [];
- problems.push({ type, fixed, time: Date.now() });
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- setJsonItem(this.store, key, problems);
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- const key = keyEndToEndSessionProblems(deviceKey);
- const problems = getJsonItem<IProblem[]>(this.store, key) || [];
- if (!problems.length) {
- return null;
- }
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- return Object.assign({}, problem, { fixed: lastProblem.fixed });
- }
- }
- if (lastProblem.fixed) {
- return null;
- } else {
- return lastProblem;
- }
- }
-
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const notifiedErrorDevices =
- getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
- const ret: IOlmDevice[] = [];
-
- for (const device of devices) {
- const { userId, deviceInfo } = device;
- if (userId in notifiedErrorDevices) {
- if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
- ret.push(device);
- safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
- }
- } else {
- ret.push(device);
- safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
- }
- }
-
- setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
-
- return ret;
- }
-
- // Inbound Group Sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- func(
- getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)),
- getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId)),
- );
- }
-
- public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
- for (let i = 0; i < this.store.length; ++i) {
- const key = this.store.key(i);
- if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
- // we can't use split, as the components we are trying to split out
- // might themselves contain '/' characters. We rely on the
- // senderKey being a (32-byte) curve25519 key, base64-encoded
- // (hence 43 characters long).
-
- func({
- senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
- sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
- sessionData: getJsonItem(this.store, key)!,
- });
- }
- }
- func(null);
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId));
- if (!existing) {
- this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData);
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void {
- setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData);
- }
-
- public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
- func(getJsonItem(this.store, KEY_DEVICE_DATA));
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
- setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
- }
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
- setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
- }
-
- public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- const result: Record<string, IRoomEncryption> = {};
- const prefix = keyEndToEndRoomsPrefix("");
-
- for (let i = 0; i < this.store.length; ++i) {
- const key = this.store.key(i);
- if (key?.startsWith(prefix)) {
- const roomId = key.slice(prefix.length);
- result[roomId] = getJsonItem(this.store, key)!;
- }
- }
- func(result);
- }
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- const sessions: ISession[] = [];
-
- for (const session in sessionsNeedingBackup) {
- if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
- // see getAllEndToEndInboundGroupSessions for the magic number explanations
- const senderKey = session.slice(0, 43);
- const sessionId = session.slice(44);
- this.getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => {
- sessions.push({
- senderKey: senderKey,
- sessionId: sessionId,
- sessionData: sessionData!,
- });
- });
- if (limit && sessions.length >= limit) {
- break;
- }
- }
- }
- return Promise.resolve(sessions);
- }
-
- public countSessionsNeedingBackup(): Promise<number> {
- const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
- }
-
- public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- const sessionsNeedingBackup =
- getJsonItem<{
- [senderKeySessionId: string]: string;
- }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- for (const session of sessions) {
- delete sessionsNeedingBackup[session.senderKey + "/" + session.sessionId];
- }
- setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
- return Promise.resolve();
- }
-
- public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- const sessionsNeedingBackup =
- getJsonItem<{
- [senderKeySessionId: string]: boolean;
- }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- for (const session of sessions) {
- sessionsNeedingBackup[session.senderKey + "/" + session.sessionId] = true;
- }
- setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
- return Promise.resolve();
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns Promise which resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- this.store.removeItem(KEY_END_TO_END_ACCOUNT);
- return Promise.resolve();
- }
-
- // Olm account
-
- public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
- const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
- func(accountPickle);
- }
-
- public storeAccount(txn: unknown, accountPickle: string): void {
- setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
- }
-
- public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
- const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
- func(keys);
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const key = getJsonItem<SecretStorePrivateKeys[K]>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
- func(key);
- }
-
- public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
- setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
- }
-
- public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
- return Promise.resolve(func(null));
- }
-}
-
-function getJsonItem<T>(store: Storage, key: string): T | null {
- try {
- // if the key is absent, store.getItem() returns null, and
- // JSON.parse(null) === null, so this returns null.
- return JSON.parse(store.getItem(key)!);
- } catch (e) {
- logger.log("Error: Failed to get key %s: %s", key, (<Error>e).message);
- logger.log((<Error>e).stack);
- }
- return null;
-}
-
-function setJsonItem<T>(store: Storage, key: string, val: T): void {
- store.setItem(key, JSON.stringify(val));
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
deleted file mode 100644
index 29ae81b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
+++ /dev/null
@@ -1,533 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { logger } from "../../logger";
-import * as utils from "../../utils";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { safeSet } from "../../utils";
-
-/**
- * Internal module. in-memory storage for e2e.
- */
-
-export class MemoryCryptoStore implements CryptoStore {
- private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
- private account: string | null = null;
- private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
- private privateKeys: Partial<SecretStorePrivateKeys> = {};
-
- private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
- private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
- private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
- private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
- private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
- // Opaque device data object
- private deviceData: IDeviceData | null = null;
- private rooms: { [roomId: string]: IRoomEncryption } = {};
- private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
- private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
- private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
-
- /**
- * Ensure the database exists and is up-to-date.
- *
- * This must be called before the store can be used.
- *
- * @returns resolves to the store.
- */
- public async startup(): Promise<CryptoStore> {
- // No startup work to do for the memory store.
- return this;
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns Promise which resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- const requestBody = request.requestBody;
-
- return utils.promiseTry(() => {
- // first see if we already have an entry for this request.
- const existing = this._getOutgoingRoomKeyRequest(requestBody);
-
- if (existing) {
- // this entry matches the request - return it.
- logger.log(
- `already have key request outstanding for ` +
- `${requestBody.room_id} / ${requestBody.session_id}: ` +
- `not sending another`,
- );
- return existing;
- }
-
- // we got to the end of the list without finding a match
- // - add the new request.
- logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
- this.outgoingRoomKeyRequests.push(request);
- return request;
- });
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
- }
-
- /**
- * Looks for existing room key request, and returns the result synchronously.
- *
- * @internal
- *
- * @param requestBody - existing request to look for
- *
- * @returns
- * the matching request, or null if not found
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
- for (const existing of this.outgoingRoomKeyRequests) {
- if (utils.deepCompare(existing.requestBody, requestBody)) {
- return existing;
- }
- }
- return null;
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- for (const req of this.outgoingRoomKeyRequests) {
- for (const state of wantedStates) {
- if (req.state === state) {
- return Promise.resolve(req);
- }
- }
- }
- return Promise.resolve(null);
- }
-
- /**
- *
- * @returns All OutgoingRoomKeyRequests in state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return Promise.resolve(this.outgoingRoomKeyRequests.filter((r) => r.state == wantedState));
- }
-
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- const results: OutgoingRoomKeyRequest[] = [];
-
- for (const req of this.outgoingRoomKeyRequests) {
- for (const state of wantedStates) {
- if (
- req.state === state &&
- req.recipients.some((recipient) => recipient.userId === userId && recipient.deviceId === deviceId)
- ) {
- results.push(req);
- }
- }
- }
- return Promise.resolve(results);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- for (const req of this.outgoingRoomKeyRequests) {
- if (req.requestId !== requestId) {
- continue;
- }
-
- if (req.state !== expectedState) {
- logger.warn(
- `Cannot update room key request from ${expectedState} ` +
- `as it was already updated to ${req.state}`,
- );
- return Promise.resolve(null);
- }
- Object.assign(req, updates);
- return Promise.resolve(req);
- }
-
- return Promise.resolve(null);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
- const req = this.outgoingRoomKeyRequests[i];
-
- if (req.requestId !== requestId) {
- continue;
- }
-
- if (req.state != expectedState) {
- logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`);
- return Promise.resolve(null);
- }
-
- this.outgoingRoomKeyRequests.splice(i, 1);
- return Promise.resolve(req);
- }
-
- return Promise.resolve(null);
- }
-
- // Olm Account
-
- public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
- func(this.account);
- }
-
- public storeAccount(txn: unknown, accountPickle: string): void {
- this.account = accountPickle;
- }
-
- public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
- func(this.crossSigningKeys);
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
- func(result || null);
- }
-
- public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
- this.crossSigningKeys = keys;
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- this.privateKeys[type] = key;
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
- func(Object.keys(this.sessions).length);
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo) => void,
- ): void {
- const deviceSessions = this.sessions[deviceKey] || {};
- func(deviceSessions[sessionId] || null);
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- func(this.sessions[deviceKey] || {});
- }
-
- public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
- Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
- Object.entries(deviceSessions).forEach(([sessionId, session]) => {
- func({
- ...session,
- deviceKey,
- sessionId,
- });
- });
- });
- }
-
- public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
- let deviceSessions = this.sessions[deviceKey];
- if (deviceSessions === undefined) {
- deviceSessions = {};
- this.sessions[deviceKey] = deviceSessions;
- }
- deviceSessions[sessionId] = sessionInfo;
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const problems = (this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []);
- problems.push({ type, fixed, time: Date.now() });
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- const problems = this.sessionProblems[deviceKey] || [];
- if (!problems.length) {
- return null;
- }
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- return Object.assign({}, problem, { fixed: lastProblem.fixed });
- }
- }
- if (lastProblem.fixed) {
- return null;
- } else {
- return lastProblem;
- }
- }
-
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const notifiedErrorDevices = this.notifiedErrorDevices;
- const ret: IOlmDevice[] = [];
-
- for (const device of devices) {
- const { userId, deviceInfo } = device;
- if (userId in notifiedErrorDevices) {
- if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
- ret.push(device);
- safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
- }
- } else {
- ret.push(device);
- safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
- }
- }
-
- return ret;
- }
-
- // Inbound Group Sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
- }
-
- public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
- for (const key of Object.keys(this.inboundGroupSessions)) {
- // we can't use split, as the components we are trying to split out
- // might themselves contain '/' characters. We rely on the
- // senderKey being a (32-byte) curve25519 key, base64-encoded
- // (hence 43 characters long).
-
- func({
- senderKey: key.slice(0, 43),
- sessionId: key.slice(44),
- sessionData: this.inboundGroupSessions[key],
- });
- }
- func(null);
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- if (this.inboundGroupSessions[k] === undefined) {
- this.inboundGroupSessions[k] = sessionData;
- }
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- this.inboundGroupSessionsWithheld[k] = sessionData;
- }
-
- // Device Data
-
- public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
- func(this.deviceData);
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
- this.deviceData = deviceData;
- }
-
- // E2E rooms
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
- this.rooms[roomId] = roomInfo;
- }
-
- public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- func(this.rooms);
- }
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- const sessions: ISession[] = [];
- for (const session in this.sessionsNeedingBackup) {
- if (this.inboundGroupSessions[session]) {
- sessions.push({
- senderKey: session.slice(0, 43),
- sessionId: session.slice(44),
- sessionData: this.inboundGroupSessions[session],
- });
- if (limit && session.length >= limit) {
- break;
- }
- }
- }
- return Promise.resolve(sessions);
- }
-
- public countSessionsNeedingBackup(): Promise<number> {
- return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
- }
-
- public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- for (const session of sessions) {
- const sessionKey = session.senderKey + "/" + session.sessionId;
- delete this.sessionsNeedingBackup[sessionKey];
- }
- return Promise.resolve();
- }
-
- public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- for (const session of sessions) {
- const sessionKey = session.senderKey + "/" + session.sessionId;
- this.sessionsNeedingBackup[sessionKey] = true;
- }
- return Promise.resolve();
- }
-
- public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
- const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
- sessions.push([senderKey, sessionId]);
- this.sharedHistoryInboundGroupSessions[roomId] = sessions;
- }
-
- public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
- return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
- }
-
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
- const parked = this.parkedSharedHistory.get(roomId) ?? [];
- parked.push(parkedData);
- this.parkedSharedHistory.set(roomId, parked);
- }
-
- public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
- const parked = this.parkedSharedHistory.get(roomId) ?? [];
- this.parkedSharedHistory.delete(roomId);
- return Promise.resolve(parked);
- }
-
- // Session key backups
-
- public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
- return Promise.resolve(func(null));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
deleted file mode 100644
index 89c700c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2020 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.
-*/
-
-/**
- * Base class for verification methods.
- */
-
-import { MatrixEvent } from "../../models/event";
-import { EventType } from "../../@types/event";
-import { logger } from "../../logger";
-import { DeviceInfo } from "../deviceinfo";
-import { newTimeoutError } from "./Error";
-import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixClient } from "../../client";
-import { VerificationRequest } from "./request/VerificationRequest";
-import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
-
-const timeoutException = new Error("Verification timed out");
-
-export class SwitchStartEventError extends Error {
- public constructor(public readonly startEvent: MatrixEvent | null) {
- super();
- }
-}
-
-export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
-
-export enum VerificationEvent {
- Cancel = "cancel",
-}
-
-export type VerificationEventHandlerMap = {
- [VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
-};
-
-export class VerificationBase<
- Events extends string,
- Arguments extends ListenerMap<Events | VerificationEvent>,
-> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
- private cancelled = false;
- private _done = false;
- private promise: Promise<void> | null = null;
- private transactionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
- protected expectedEvent?: string;
- private resolve?: () => void;
- private reject?: (e: Error | MatrixEvent) => void;
- private resolveEvent?: (e: MatrixEvent) => void;
- private rejectEvent?: (e: Error) => void;
- private started?: boolean;
-
- /**
- * Base class for verification methods.
- *
- * <p>Once a verifier object is created, the verification can be started by
- * calling the verify() method, which will return a promise that will
- * resolve when the verification is completed, or reject if it could not
- * complete.</p>
- *
- * <p>Subclasses must have a NAME class property.</p>
- *
- * @param channel - the verification channel to send verification messages over.
- * TODO: Channel types
- *
- * @param baseApis - base matrix api interface
- *
- * @param userId - the user ID that is being verified
- *
- * @param deviceId - the device ID that is being verified
- *
- * @param startEvent - the m.key.verification.start event that
- * initiated this verification, if any
- *
- * @param request - the key verification request object related to
- * this verification, if any
- */
- public constructor(
- public readonly channel: IVerificationChannel,
- public readonly baseApis: MatrixClient,
- public readonly userId: string,
- public readonly deviceId: string,
- public startEvent: MatrixEvent | null,
- public readonly request: VerificationRequest,
- ) {
- super();
- }
-
- public get initiatedByMe(): boolean {
- // if there is no start event yet,
- // we probably want to send it,
- // which happens if we initiate
- if (!this.startEvent) {
- return true;
- }
- const sender = this.startEvent.getSender();
- const content = this.startEvent.getContent();
- return sender === this.baseApis.getUserId() && content.from_device === this.baseApis.getDeviceId();
- }
-
- public get hasBeenCancelled(): boolean {
- return this.cancelled;
- }
-
- private resetTimer(): void {
- logger.info("Refreshing/starting the verification transaction timeout timer");
- if (this.transactionTimeoutTimer !== null) {
- clearTimeout(this.transactionTimeoutTimer);
- }
- this.transactionTimeoutTimer = setTimeout(() => {
- if (!this._done && !this.cancelled) {
- logger.info("Triggering verification timeout");
- this.cancel(timeoutException);
- }
- }, 10 * 60 * 1000); // 10 minutes
- }
-
- private endTimer(): void {
- if (this.transactionTimeoutTimer !== null) {
- clearTimeout(this.transactionTimeoutTimer);
- this.transactionTimeoutTimer = null;
- }
- }
-
- protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
- return this.channel.send(type, uncompletedContent);
- }
-
- protected waitForEvent(type: string): Promise<MatrixEvent> {
- if (this._done) {
- return Promise.reject(new Error("Verification is already done"));
- }
- const existingEvent = this.request.getEventFromOtherParty(type);
- if (existingEvent) {
- return Promise.resolve(existingEvent);
- }
-
- this.expectedEvent = type;
- return new Promise((resolve, reject) => {
- this.resolveEvent = resolve;
- this.rejectEvent = reject;
- });
- }
-
- public canSwitchStartEvent(event: MatrixEvent): boolean {
- return false;
- }
-
- public switchStartEvent(event: MatrixEvent): void {
- if (this.canSwitchStartEvent(event)) {
- logger.log("Verification Base: switching verification start event", { restartingFlow: !!this.rejectEvent });
- if (this.rejectEvent) {
- const reject = this.rejectEvent;
- this.rejectEvent = undefined;
- reject(new SwitchStartEventError(event));
- } else {
- this.startEvent = event;
- }
- }
- }
-
- public handleEvent(e: MatrixEvent): void {
- if (this._done) {
- return;
- } else if (e.getType() === this.expectedEvent) {
- // if we receive an expected m.key.verification.done, then just
- // ignore it, since we don't need to do anything about it
- if (this.expectedEvent !== EventType.KeyVerificationDone) {
- this.expectedEvent = undefined;
- this.rejectEvent = undefined;
- this.resetTimer();
- this.resolveEvent?.(e);
- }
- } else if (e.getType() === EventType.KeyVerificationCancel) {
- const reject = this.reject;
- this.reject = undefined;
- // there is only promise to reject if verify has been called
- if (reject) {
- const content = e.getContent();
- const { reason, code } = content;
- reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`));
- }
- } else if (this.expectedEvent) {
- // only cancel if there is an event expected.
- // if there is no event expected, it means verify() wasn't called
- // and we're just replaying the timeline events when syncing
- // after a refresh when the events haven't been stored in the cache yet.
- const exception = new Error(
- "Unexpected message: expecting " + this.expectedEvent + " but got " + e.getType(),
- );
- this.expectedEvent = undefined;
- if (this.rejectEvent) {
- const reject = this.rejectEvent;
- this.rejectEvent = undefined;
- reject(exception);
- }
- this.cancel(exception);
- }
- }
-
- public async done(): Promise<KeysDuringVerification | void> {
- this.endTimer(); // always kill the activity timer
- if (!this._done) {
- this.request.onVerifierFinished();
- this.resolve?.();
- return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
- }
- }
-
- public cancel(e: Error | MatrixEvent): void {
- this.endTimer(); // always kill the activity timer
- if (!this._done) {
- this.cancelled = true;
- this.request.onVerifierCancelled();
- if (this.userId && this.deviceId) {
- // send a cancellation to the other user (if it wasn't
- // cancelled by the other user)
- if (e === timeoutException) {
- const timeoutEvent = newTimeoutError();
- this.send(timeoutEvent.getType(), timeoutEvent.getContent());
- } else if (e instanceof MatrixEvent) {
- const sender = e.getSender();
- if (sender !== this.userId) {
- const content = e.getContent();
- if (e.getType() === EventType.KeyVerificationCancel) {
- content.code = content.code || "m.unknown";
- content.reason = content.reason || content.body || "Unknown reason";
- this.send(EventType.KeyVerificationCancel, content);
- } else {
- this.send(EventType.KeyVerificationCancel, {
- code: "m.unknown",
- reason: content.body || "Unknown reason",
- });
- }
- }
- } else {
- this.send(EventType.KeyVerificationCancel, {
- code: "m.unknown",
- reason: e.toString(),
- });
- }
- }
- if (this.promise !== null) {
- // when we cancel without a promise, we end up with a promise
- // but no reject function. If cancel is called again, we'd error.
- if (this.reject) this.reject(e);
- } else {
- // FIXME: this causes an "Uncaught promise" console message
- // if nothing ends up chaining this promise.
- this.promise = Promise.reject(e);
- }
- // Also emit a 'cancel' event that the app can listen for to detect cancellation
- // before calling verify()
- this.emit(VerificationEvent.Cancel, e);
- }
- }
-
- /**
- * Begin the key verification
- *
- * @returns Promise which resolves when the verification has
- * completed.
- */
- public verify(): Promise<void> {
- if (this.promise) return this.promise;
-
- this.promise = new Promise((resolve, reject) => {
- this.resolve = (...args): void => {
- this._done = true;
- this.endTimer();
- resolve(...args);
- };
- this.reject = (e: Error | MatrixEvent): void => {
- this._done = true;
- this.endTimer();
- reject(e);
- };
- });
- if (this.doVerification && !this.started) {
- this.started = true;
- this.resetTimer(); // restart the timeout
- new Promise<void>((resolve, reject) => {
- const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
- if (crossSignId === this.deviceId) {
- reject(new Error("Device ID is the same as the cross-signing ID"));
- }
- resolve();
- })
- .then(() => this.doVerification!())
- .then(this.done.bind(this), this.cancel.bind(this));
- }
- return this.promise;
- }
-
- protected doVerification?: () => Promise<void>;
-
- protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
- // we try to verify all the keys that we're told about, but we might
- // not know about all of them, so keep track of the keys that we know
- // about, and ignore the rest
- const verifiedDevices: [string, string, string][] = [];
-
- for (const [keyId, keyInfo] of Object.entries(keys)) {
- const deviceId = keyId.split(":", 2)[1];
- const device = this.baseApis.getStoredDevice(userId, deviceId);
- if (device) {
- verifier(keyId, device, keyInfo);
- verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
- } else {
- const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
- verifier(
- keyId,
- DeviceInfo.fromStorage(
- {
- keys: {
- [keyId]: deviceId,
- },
- },
- deviceId,
- ),
- keyInfo,
- );
- verifiedDevices.push([deviceId, keyId, deviceId]);
- } else {
- logger.warn(`verification: Could not find device ${deviceId} to verify`);
- }
- }
- }
-
- // if none of the keys could be verified, then error because the app
- // should be informed about that
- if (!verifiedDevices.length) {
- throw new Error("No devices could be verified");
- }
-
- logger.info("Verification completed! Marking devices verified: ", verifiedDevices);
- // TODO: There should probably be a batch version of this, otherwise it's going
- // to upload each signature in a separate API call which is silly because the
- // API supports as many signatures as you like.
- for (const [deviceId, keyId, key] of verifiedDevices) {
- await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
- }
-
- // if one of the user's own devices is being marked as verified / unverified,
- // check the key backup status, since whether or not we use this depends on
- // whether it has a signature from a verified device
- if (userId == this.baseApis.credentials.userId) {
- await this.baseApis.checkKeyBackup();
- }
- }
-
- public get events(): string[] | undefined {
- return undefined;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
deleted file mode 100644
index da73ebb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Error messages.
- */
-
-import { MatrixEvent } from "../../models/event";
-import { EventType } from "../../@types/event";
-
-export function newVerificationError(code: string, reason: string, extraData?: Record<string, any>): MatrixEvent {
- const content = Object.assign({}, { code, reason }, extraData);
- return new MatrixEvent({
- type: EventType.KeyVerificationCancel,
- content,
- });
-}
-
-export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
- return function (extraData?: Record<string, any>) {
- return newVerificationError(code, reason, extraData);
- };
-}
-
-/**
- * The verification was cancelled by the user.
- */
-export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
-
-/**
- * The verification timed out.
- */
-export const newTimeoutError = errorFactory("m.timeout", "Timed out");
-
-/**
- * An unknown method was selected.
- */
-export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
-
-/**
- * An unexpected message was sent.
- */
-export const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message");
-
-/**
- * The key does not match.
- */
-export const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch");
-
-/**
- * An invalid message was sent.
- */
-export const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message");
-
-export function errorFromEvent(event: MatrixEvent): { code: string; reason: string } {
- const content = event.getContent();
- if (content) {
- const { code, reason } = content;
- return { code, reason };
- } else {
- return { code: "Unknown error", reason: "m.unknown" };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
deleted file mode 100644
index c437e0c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2020 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.
-*/
-
-/**
- * Verification method that is illegal to have (cannot possibly
- * do verification with this method).
- */
-
-import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixClient } from "../../client";
-import { MatrixEvent } from "../../models/event";
-import { VerificationRequest } from "./request/VerificationRequest";
-
-export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
- public static factory(
- channel: IVerificationChannel,
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
- startEvent: MatrixEvent,
- request: VerificationRequest,
- ): IllegalMethod {
- return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- // Typically the name will be something else, but to complete
- // the contract we offer a default one here.
- return "org.matrix.illegal_method";
- }
-
- protected doVerification = async (): Promise<void> => {
- throw new Error("Verification is not possible with this method");
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
deleted file mode 100644
index bfb532e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * QR code key verification.
- */
-
-import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
-import { newKeyMismatchError, newUserCancelledError } from "./Error";
-import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
-import { logger } from "../../logger";
-import { VerificationRequest } from "./request/VerificationRequest";
-import { MatrixClient } from "../../client";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixEvent } from "../../models/event";
-
-export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
-export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
-
-interface IReciprocateQr {
- confirm(): void;
- cancel(): void;
-}
-
-export enum QrCodeEvent {
- ShowReciprocateQr = "show_reciprocate_qr",
-}
-
-type EventHandlerMap = {
- [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
-} & VerificationEventHandlerMap;
-
-export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
- public reciprocateQREvent?: IReciprocateQr;
-
- public static factory(
- channel: IVerificationChannel,
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
- startEvent: MatrixEvent,
- request: VerificationRequest,
- ): ReciprocateQRCode {
- return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- return "m.reciprocate.v1";
- }
-
- protected doVerification = async (): Promise<void> => {
- if (!this.startEvent) {
- // TODO: Support scanning QR codes
- throw new Error("It is not currently possible to start verification" + "with this method yet.");
- }
-
- const { qrCodeData } = this.request;
- // 1. check the secret
- if (this.startEvent.getContent()["secret"] !== qrCodeData?.encodedSharedSecret) {
- throw newKeyMismatchError();
- }
-
- // 2. ask if other user shows shield as well
- await new Promise<void>((resolve, reject) => {
- this.reciprocateQREvent = {
- confirm: resolve,
- cancel: () => reject(newUserCancelledError()),
- };
- this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
- });
-
- // 3. determine key to sign / mark as trusted
- const keys: Record<string, string> = {};
-
- switch (qrCodeData?.mode) {
- case Mode.VerifyOtherUser: {
- // add master key to keys to be signed, only if we're not doing self-verification
- const masterKey = qrCodeData.otherUserMasterKey;
- keys[`ed25519:${masterKey}`] = masterKey!;
- break;
- }
- case Mode.VerifySelfTrusted: {
- const deviceId = this.request.targetDevice.deviceId;
- keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!;
- break;
- }
- case Mode.VerifySelfUntrusted: {
- const masterKey = qrCodeData.myMasterKey;
- keys[`ed25519:${masterKey}`] = masterKey!;
- break;
- }
- }
-
- // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
- await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
- // make sure the device has the expected keys
- const targetKey = keys[keyId];
- if (!targetKey) throw newKeyMismatchError();
-
- if (keyInfo !== targetKey) {
- logger.error("key ID from key info does not match");
- throw newKeyMismatchError();
- }
- for (const deviceKeyId in device.keys) {
- if (!deviceKeyId.startsWith("ed25519")) continue;
- const deviceTargetKey = keys[deviceKeyId];
- if (!deviceTargetKey) throw newKeyMismatchError();
- if (device.keys[deviceKeyId] !== deviceTargetKey) {
- logger.error("master key does not match");
- throw newKeyMismatchError();
- }
- }
- });
- };
-}
-
-const CODE_VERSION = 0x02; // the version of binary QR codes we support
-const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
-
-enum Mode {
- VerifyOtherUser = 0x00, // Verifying someone who isn't us
- VerifySelfTrusted = 0x01, // We trust the master key
- VerifySelfUntrusted = 0x02, // We do not trust the master key
-}
-
-interface IQrData {
- prefix: string;
- version: number;
- mode: Mode;
- transactionId?: string;
- firstKeyB64: string;
- secondKeyB64: string;
- secretB64: string;
-}
-
-export class QRCodeData {
- public constructor(
- public readonly mode: Mode,
- private readonly sharedSecret: string,
- // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
- public readonly otherUserMasterKey: string | null,
- // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
- public readonly otherDeviceKey: string | null,
- // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
- public readonly myMasterKey: string | null,
- private readonly buffer: Buffer,
- ) {}
-
- public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
- const sharedSecret = QRCodeData.generateSharedSecret();
- const mode = QRCodeData.determineMode(request, client);
- let otherUserMasterKey: string | null = null;
- let otherDeviceKey: string | null = null;
- let myMasterKey: string | null = null;
- if (mode === Mode.VerifyOtherUser) {
- const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId);
- otherUserMasterKey = otherUserCrossSigningInfo!.getId("master");
- } else if (mode === Mode.VerifySelfTrusted) {
- otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
- } else if (mode === Mode.VerifySelfUntrusted) {
- const myUserId = client.getUserId()!;
- const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
- myMasterKey = myCrossSigningInfo!.getId("master");
- }
- const qrData = QRCodeData.generateQrData(
- request,
- client,
- mode,
- sharedSecret,
- otherUserMasterKey!,
- otherDeviceKey!,
- myMasterKey!,
- );
- const buffer = QRCodeData.generateBuffer(qrData);
- return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
- }
-
- /**
- * The unpadded base64 encoded shared secret.
- */
- public get encodedSharedSecret(): string {
- return this.sharedSecret;
- }
-
- public getBuffer(): Buffer {
- return this.buffer;
- }
-
- private static generateSharedSecret(): string {
- const secretBytes = new Uint8Array(11);
- global.crypto.getRandomValues(secretBytes);
- return encodeUnpaddedBase64(secretBytes);
- }
-
- private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
- const myUserId = client.getUserId()!;
- const otherDevice = request.targetDevice;
- const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined;
- if (!device) {
- throw new Error("could not find device " + otherDevice?.deviceId);
- }
- return device.getFingerprint();
- }
-
- private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
- const myUserId = client.getUserId();
- const otherUserId = request.otherUserId;
-
- let mode = Mode.VerifyOtherUser;
- if (myUserId === otherUserId) {
- // Mode changes depending on whether or not we trust the master cross signing key
- const myTrust = client.checkUserTrust(myUserId);
- if (myTrust.isCrossSigningVerified()) {
- mode = Mode.VerifySelfTrusted;
- } else {
- mode = Mode.VerifySelfUntrusted;
- }
- }
- return mode;
- }
-
- private static generateQrData(
- request: VerificationRequest,
- client: MatrixClient,
- mode: Mode,
- encodedSharedSecret: string,
- otherUserMasterKey?: string,
- otherDeviceKey?: string,
- myMasterKey?: string,
- ): IQrData {
- const myUserId = client.getUserId()!;
- const transactionId = request.channel.transactionId;
- const qrData: IQrData = {
- prefix: BINARY_PREFIX,
- version: CODE_VERSION,
- mode,
- transactionId,
- firstKeyB64: "", // worked out shortly
- secondKeyB64: "", // worked out shortly
- secretB64: encodedSharedSecret,
- };
-
- const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
-
- if (mode === Mode.VerifyOtherUser) {
- // First key is our master cross signing key
- qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
- // Second key is the other user's master cross signing key
- qrData.secondKeyB64 = otherUserMasterKey!;
- } else if (mode === Mode.VerifySelfTrusted) {
- // First key is our master cross signing key
- qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
- qrData.secondKeyB64 = otherDeviceKey!;
- } else if (mode === Mode.VerifySelfUntrusted) {
- // First key is our device's key
- qrData.firstKeyB64 = client.getDeviceEd25519Key()!;
- // Second key is what we think our master cross signing key is
- qrData.secondKeyB64 = myMasterKey!;
- }
- return qrData;
- }
-
- private static generateBuffer(qrData: IQrData): Buffer {
- let buf = Buffer.alloc(0); // we'll concat our way through life
-
- const appendByte = (b: number): void => {
- const tmpBuf = Buffer.from([b]);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendInt = (i: number): void => {
- const tmpBuf = Buffer.alloc(2);
- tmpBuf.writeInt16BE(i, 0);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => {
- const tmpBuf = Buffer.from(s, enc);
- if (withLengthPrefix) appendInt(tmpBuf.byteLength);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendEncBase64 = (b64: string): void => {
- const b = decodeBase64(b64);
- const tmpBuf = Buffer.from(b);
- buf = Buffer.concat([buf, tmpBuf]);
- };
-
- // Actually build the buffer for the QR code
- appendStr(qrData.prefix, "ascii", false);
- appendByte(qrData.version);
- appendByte(qrData.mode);
- appendStr(qrData.transactionId!, "utf-8");
- appendEncBase64(qrData.firstKeyB64);
- appendEncBase64(qrData.secondKeyB64);
- appendEncBase64(qrData.secretB64);
-
- return buf;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
deleted file mode 100644
index a8d237d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Short Authentication String (SAS) verification.
- */
-
-import anotherjson from "another-json";
-import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
-
-import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
-import {
- errorFactory,
- newInvalidMessageError,
- newKeyMismatchError,
- newUnknownMethodError,
- newUserCancelledError,
-} from "./Error";
-import { logger } from "../../logger";
-import { IContent, MatrixEvent } from "../../models/event";
-import { generateDecimalSas } from "./SASDecimal";
-import { EventType } from "../../@types/event";
-
-const START_TYPE = EventType.KeyVerificationStart;
-
-const EVENTS = [EventType.KeyVerificationAccept, EventType.KeyVerificationKey, EventType.KeyVerificationMac];
-
-let olmutil: Utility;
-
-const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short authentication string");
-
-const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment");
-
-type EmojiMapping = [emoji: string, name: string];
-
-const emojiMapping: EmojiMapping[] = [
- ["🐶", "dog"], // 0
- ["🐱", "cat"], // 1
- ["🦁", "lion"], // 2
- ["🐎", "horse"], // 3
- ["🦄", "unicorn"], // 4
- ["🐷", "pig"], // 5
- ["🐘", "elephant"], // 6
- ["🐰", "rabbit"], // 7
- ["🐼", "panda"], // 8
- ["🐓", "rooster"], // 9
- ["🐧", "penguin"], // 10
- ["🐢", "turtle"], // 11
- ["🐟", "fish"], // 12
- ["🐙", "octopus"], // 13
- ["🦋", "butterfly"], // 14
- ["🌷", "flower"], // 15
- ["🌳", "tree"], // 16
- ["🌵", "cactus"], // 17
- ["🍄", "mushroom"], // 18
- ["🌏", "globe"], // 19
- ["🌙", "moon"], // 20
- ["☁️", "cloud"], // 21
- ["🔥", "fire"], // 22
- ["🍌", "banana"], // 23
- ["🍎", "apple"], // 24
- ["🍓", "strawberry"], // 25
- ["🌽", "corn"], // 26
- ["🍕", "pizza"], // 27
- ["🎂", "cake"], // 28
- ["❤️", "heart"], // 29
- ["🙂", "smiley"], // 30
- ["🤖", "robot"], // 31
- ["🎩", "hat"], // 32
- ["👓", "glasses"], // 33
- ["🔧", "spanner"], // 34
- ["🎅", "santa"], // 35
- ["👍", "thumbs up"], // 36
- ["☂️", "umbrella"], // 37
- ["⌛", "hourglass"], // 38
- ["⏰", "clock"], // 39
- ["🎁", "gift"], // 40
- ["💡", "light bulb"], // 41
- ["📕", "book"], // 42
- ["✏️", "pencil"], // 43
- ["📎", "paperclip"], // 44
- ["✂️", "scissors"], // 45
- ["🔒", "lock"], // 46
- ["🔑", "key"], // 47
- ["🔨", "hammer"], // 48
- ["☎️", "telephone"], // 49
- ["🏁", "flag"], // 50
- ["🚂", "train"], // 51
- ["🚲", "bicycle"], // 52
- ["✈️", "aeroplane"], // 53
- ["🚀", "rocket"], // 54
- ["🏆", "trophy"], // 55
- ["⚽", "ball"], // 56
- ["🎸", "guitar"], // 57
- ["🎺", "trumpet"], // 58
- ["🔔", "bell"], // 59
- ["⚓️", "anchor"], // 60
- ["🎧", "headphones"], // 61
- ["📁", "folder"], // 62
- ["📌", "pin"], // 63
-];
-
-function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
- const emojis = [
- // just like base64 encoding
- sasBytes[0] >> 2,
- ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4),
- ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6),
- sasBytes[2] & 0x3f,
- sasBytes[3] >> 2,
- ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4),
- ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6),
- ];
-
- return emojis.map((num) => emojiMapping[num]);
-}
-
-const sasGenerators = {
- decimal: generateDecimalSas,
- emoji: generateEmojiSas,
-} as const;
-
-export interface IGeneratedSas {
- decimal?: [number, number, number];
- emoji?: EmojiMapping[];
-}
-
-export interface ISasEvent {
- sas: IGeneratedSas;
- confirm(): Promise<void>;
- cancel(): void;
- mismatch(): void;
-}
-
-function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
- const sas: IGeneratedSas = {};
- for (const method of methods) {
- if (method in sasGenerators) {
- // @ts-ignore - ts doesn't like us mixing types like this
- sas[method] = sasGenerators[method](Array.from(sasBytes));
- }
- }
- return sas;
-}
-
-const macMethods = {
- "hkdf-hmac-sha256": "calculate_mac",
- "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
- "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64",
- "hmac-sha256": "calculate_mac_long_kdf",
-} as const;
-
-type MacMethod = keyof typeof macMethods;
-
-function calculateMAC(olmSAS: OlmSAS, method: MacMethod) {
- return function (input: string, info: string): string {
- const mac = olmSAS[macMethods[method]](input, info);
- logger.log("SAS calculateMAC:", method, [input, info], mac);
- return mac;
- };
-}
-
-const calculateKeyAgreement = {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- "curve25519-hkdf-sha256": function (sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
- const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|` + `${sas.ourSASPubKey}|`;
- const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
- const sasInfo =
- "MATRIX_KEY_VERIFICATION_SAS|" +
- (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
- sas.channel.transactionId;
- return olmSAS.generate_bytes(sasInfo, bytes);
- },
- "curve25519": function (sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
- const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
- const theirInfo = `${sas.userId}${sas.deviceId}`;
- const sasInfo =
- "MATRIX_KEY_VERIFICATION_SAS" +
- (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
- sas.channel.transactionId;
- return olmSAS.generate_bytes(sasInfo, bytes);
- },
-} as const;
-
-type KeyAgreement = keyof typeof calculateKeyAgreement;
-
-/* lists of algorithms/methods that are supported. The key agreement, hashes,
- * and MAC lists should be sorted in order of preference (most preferred
- * first).
- */
-const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
-const HASHES_LIST = ["sha256"];
-const MAC_LIST: MacMethod[] = [
- "hkdf-hmac-sha256.v2",
- "org.matrix.msc3783.hkdf-hmac-sha256",
- "hkdf-hmac-sha256",
- "hmac-sha256",
-];
-const SAS_LIST = Object.keys(sasGenerators);
-
-const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
-const HASHES_SET = new Set(HASHES_LIST);
-const MAC_SET = new Set(MAC_LIST);
-const SAS_SET = new Set(SAS_LIST);
-
-function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
- return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : [];
-}
-
-export enum SasEvent {
- ShowSas = "show_sas",
-}
-
-type EventHandlerMap = {
- [SasEvent.ShowSas]: (sas: ISasEvent) => void;
-} & VerificationEventHandlerMap;
-
-export class SAS extends Base<SasEvent, EventHandlerMap> {
- private waitingForAccept?: boolean;
- public ourSASPubKey?: string;
- public theirSASPubKey?: string;
- public sasEvent?: ISasEvent;
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- return "m.sas.v1";
- }
-
- public get events(): string[] {
- return EVENTS;
- }
-
- protected doVerification = async (): Promise<void> => {
- await global.Olm.init();
- olmutil = olmutil || new global.Olm.Utility();
-
- // make sure user's keys are downloaded
- await this.baseApis.downloadKeys([this.userId]);
-
- let retry = false;
- do {
- try {
- if (this.initiatedByMe) {
- return await this.doSendVerification();
- } else {
- return await this.doRespondVerification();
- }
- } catch (err) {
- if (err instanceof SwitchStartEventError) {
- // this changes what initiatedByMe returns
- this.startEvent = err.startEvent;
- retry = true;
- } else {
- throw err;
- }
- }
- } while (retry);
- };
-
- public canSwitchStartEvent(event: MatrixEvent): boolean {
- if (event.getType() !== START_TYPE) {
- return false;
- }
- const content = event.getContent();
- return content?.method === SAS.NAME && !!this.waitingForAccept;
- }
-
- private async sendStart(): Promise<Record<string, any>> {
- const startContent = this.channel.completeContent(START_TYPE, {
- method: SAS.NAME,
- from_device: this.baseApis.deviceId,
- key_agreement_protocols: KEY_AGREEMENT_LIST,
- hashes: HASHES_LIST,
- message_authentication_codes: MAC_LIST,
- // FIXME: allow app to specify what SAS methods can be used
- short_authentication_string: SAS_LIST,
- });
- await this.channel.sendCompleted(START_TYPE, startContent);
- return startContent;
- }
-
- private async verifyAndCheckMAC(
- keyAgreement: KeyAgreement,
- sasMethods: string[],
- olmSAS: OlmSAS,
- macMethod: MacMethod,
- ): Promise<void> {
- const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
- const verifySAS = new Promise<void>((resolve, reject) => {
- this.sasEvent = {
- sas: generateSas(sasBytes, sasMethods),
- confirm: async (): Promise<void> => {
- try {
- await this.sendMAC(olmSAS, macMethod);
- resolve();
- } catch (err) {
- reject(err);
- }
- },
- cancel: () => reject(newUserCancelledError()),
- mismatch: () => reject(newMismatchedSASError()),
- };
- this.emit(SasEvent.ShowSas, this.sasEvent);
- });
-
- const [e] = await Promise.all([
- this.waitForEvent(EventType.KeyVerificationMac).then((e) => {
- // we don't expect any more messages from the other
- // party, and they may send a m.key.verification.done
- // when they're done on their end
- this.expectedEvent = EventType.KeyVerificationDone;
- return e;
- }),
- verifySAS,
- ]);
- const content = e.getContent();
- await this.checkMAC(olmSAS, content, macMethod);
- }
-
- private async doSendVerification(): Promise<void> {
- this.waitingForAccept = true;
- let startContent;
- if (this.startEvent) {
- startContent = this.channel.completedContentFromEvent(this.startEvent);
- } else {
- startContent = await this.sendStart();
- }
-
- // we might have switched to a different start event,
- // but was we didn't call _waitForEvent there was no
- // call that could throw yet. So check manually that
- // we're still on the initiator side
- if (!this.initiatedByMe) {
- throw new SwitchStartEventError(this.startEvent);
- }
-
- let e: MatrixEvent;
- try {
- e = await this.waitForEvent(EventType.KeyVerificationAccept);
- } finally {
- this.waitingForAccept = false;
- }
- let content = e.getContent();
- const sasMethods = intersection(content.short_authentication_string, SAS_SET);
- if (
- !(
- KEY_AGREEMENT_SET.has(content.key_agreement_protocol) &&
- HASHES_SET.has(content.hash) &&
- MAC_SET.has(content.message_authentication_code) &&
- sasMethods.length
- )
- ) {
- throw newUnknownMethodError();
- }
- if (typeof content.commitment !== "string") {
- throw newInvalidMessageError();
- }
- const keyAgreement = content.key_agreement_protocol;
- const macMethod = content.message_authentication_code;
- const hashCommitment = content.commitment;
- const olmSAS = new global.Olm.SAS();
- try {
- this.ourSASPubKey = olmSAS.get_pubkey();
- await this.send(EventType.KeyVerificationKey, {
- key: this.ourSASPubKey,
- });
-
- e = await this.waitForEvent(EventType.KeyVerificationKey);
- // FIXME: make sure event is properly formed
- content = e.getContent();
- const commitmentStr = content.key + anotherjson.stringify(startContent);
- // TODO: use selected hash function (when we support multiple)
- if (olmutil.sha256(commitmentStr) !== hashCommitment) {
- throw newMismatchedCommitmentError();
- }
- this.theirSASPubKey = content.key;
- olmSAS.set_their_key(content.key);
-
- await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
- } finally {
- olmSAS.free();
- }
- }
-
- private async doRespondVerification(): Promise<void> {
- // as m.related_to is not included in the encrypted content in e2e rooms,
- // we need to make sure it is added
- let content = this.channel.completedContentFromEvent(this.startEvent!);
-
- // Note: we intersect using our pre-made lists, rather than the sets,
- // so that the result will be in our order of preference. Then
- // fetching the first element from the array will give our preferred
- // method out of the ones offered by the other party.
- const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
- const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
- const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
- // FIXME: allow app to specify what SAS methods can be used
- const sasMethods = intersection(content.short_authentication_string, SAS_SET);
- if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
- throw newUnknownMethodError();
- }
-
- const olmSAS = new global.Olm.SAS();
- try {
- const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
- await this.send(EventType.KeyVerificationAccept, {
- key_agreement_protocol: keyAgreement,
- hash: hashMethod,
- message_authentication_code: macMethod,
- short_authentication_string: sasMethods,
- // TODO: use selected hash function (when we support multiple)
- commitment: olmutil.sha256(commitmentStr),
- });
-
- const e = await this.waitForEvent(EventType.KeyVerificationKey);
- // FIXME: make sure event is properly formed
- content = e.getContent();
- this.theirSASPubKey = content.key;
- olmSAS.set_their_key(content.key);
- this.ourSASPubKey = olmSAS.get_pubkey();
- await this.send(EventType.KeyVerificationKey, {
- key: this.ourSASPubKey,
- });
-
- await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
- } finally {
- olmSAS.free();
- }
- }
-
- private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise<void> {
- const mac: Record<string, string> = {};
- const keyList: string[] = [];
- const baseInfo =
- "MATRIX_KEY_VERIFICATION_MAC" +
- this.baseApis.getUserId() +
- this.baseApis.deviceId +
- this.userId +
- this.deviceId +
- this.channel.transactionId;
-
- const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
- mac[deviceKeyId] = calculateMAC(olmSAS, method)(this.baseApis.getDeviceEd25519Key()!, baseInfo + deviceKeyId);
- keyList.push(deviceKeyId);
-
- const crossSigningId = this.baseApis.getCrossSigningId();
- if (crossSigningId) {
- const crossSigningKeyId = `ed25519:${crossSigningId}`;
- mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(crossSigningId, baseInfo + crossSigningKeyId);
- keyList.push(crossSigningKeyId);
- }
-
- const keys = calculateMAC(olmSAS, method)(keyList.sort().join(","), baseInfo + "KEY_IDS");
- return this.send(EventType.KeyVerificationMac, { mac, keys });
- }
-
- private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise<void> {
- const baseInfo =
- "MATRIX_KEY_VERIFICATION_MAC" +
- this.userId +
- this.deviceId +
- this.baseApis.getUserId() +
- this.baseApis.deviceId +
- this.channel.transactionId;
-
- if (
- content.keys !==
- calculateMAC(olmSAS, method)(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")
- ) {
- throw newKeyMismatchError();
- }
-
- await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
- if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) {
- throw newKeyMismatchError();
- }
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
deleted file mode 100644
index 0cb4630..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/**
- * Implementation of decimal encoding of SAS as per:
- * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal
- * @param sasBytes - the five bytes generated by HKDF
- * @returns the derived three numbers between 1000 and 9191 inclusive
- */
-export function generateDecimalSas(sasBytes: number[]): [number, number, number] {
- /*
- * +--------+--------+--------+--------+--------+
- * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
- * +--------+--------+--------+--------+--------+
- * bits: 87654321 87654321 87654321 87654321 87654321
- * \____________/\_____________/\____________/
- * 1st number 2nd number 3rd number
- */
- return [
- ((sasBytes[0] << 5) | (sasBytes[1] >> 3)) + 1000,
- (((sasBytes[1] & 0x7) << 10) | (sasBytes[2] << 2) | (sasBytes[3] >> 6)) + 1000,
- (((sasBytes[3] & 0x3f) << 7) | (sasBytes[4] >> 1)) + 1000,
- ];
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
deleted file mode 100644
index 48415f9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2021 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 { MatrixEvent } from "../../../models/event";
-import { VerificationRequest } from "./VerificationRequest";
-
-export interface IVerificationChannel {
- request?: VerificationRequest;
- readonly userId?: string;
- readonly roomId?: string;
- readonly deviceId?: string;
- readonly transactionId?: string;
- readonly receiveStartFromOtherDevices?: boolean;
- getTimestamp(event: MatrixEvent): number;
- send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
- completeContent(type: string, content: Record<string, any>): Record<string, any>;
- sendCompleted(type: string, content: Record<string, any>): Promise<void>;
- completedContentFromEvent(event: MatrixEvent): Record<string, any>;
- canCreateRequest(type: string): boolean;
- handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
deleted file mode 100644
index ff11bf1..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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 { VerificationRequest, REQUEST_TYPE, READY_TYPE, START_TYPE } from "./VerificationRequest";
-import { logger } from "../../../logger";
-import { IVerificationChannel } from "./Channel";
-import { EventType } from "../../../@types/event";
-import { MatrixClient } from "../../../client";
-import { MatrixEvent } from "../../../models/event";
-import { IRequestsMap } from "../..";
-
-const MESSAGE_TYPE = EventType.RoomMessage;
-const M_REFERENCE = "m.reference";
-const M_RELATES_TO = "m.relates_to";
-
-/**
- * A key verification channel that sends verification events in the timeline of a room.
- * Uses the event id of the initial m.key.verification.request event as a transaction id.
- */
-export class InRoomChannel implements IVerificationChannel {
- private requestEventId?: string;
-
- /**
- * @param client - the matrix client, to send messages with and get current user & device from.
- * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user.
- * @param userId - id of user that the verification request is directed at, should be present in the room.
- */
- public constructor(private readonly client: MatrixClient, public readonly roomId: string, public userId?: string) {}
-
- public get receiveStartFromOtherDevices(): boolean {
- return true;
- }
-
- /** The transaction id generated/used by this verification channel */
- public get transactionId(): string | undefined {
- return this.requestEventId;
- }
-
- public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string | undefined {
- const type = InRoomChannel.getEventType(event);
- if (type !== REQUEST_TYPE) {
- return;
- }
- const ownUserId = client.getUserId();
- const sender = event.getSender();
- const content = event.getContent();
- const receiver = content.to;
-
- if (sender === ownUserId) {
- return receiver;
- } else if (receiver === ownUserId) {
- return sender;
- }
- }
-
- /**
- * @param event - the event to get the timestamp of
- * @returns the timestamp when the event was sent
- */
- public getTimestamp(event: MatrixEvent): number {
- return event.getTs();
- }
-
- /**
- * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
- * @param type - the event type to check
- * @returns boolean flag
- */
- public static canCreateRequest(type: string): boolean {
- return type === REQUEST_TYPE;
- }
-
- public canCreateRequest(type: string): boolean {
- return InRoomChannel.canCreateRequest(type);
- }
-
- /**
- * Extract the transaction id used by a given key verification event, if any
- * @param event - the event
- * @returns the transaction id
- */
- public static getTransactionId(event: MatrixEvent): string | undefined {
- if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
- return event.getId();
- } else {
- const relation = event.getRelation();
- if (relation?.rel_type === M_REFERENCE) {
- return relation.event_id;
- }
- }
- }
-
- /**
- * Checks whether this event is a well-formed key verification event.
- * This only does checks that don't rely on the current state of a potentially already channel
- * so we can prevent channels being created by invalid events.
- * `handleEvent` can do more checks and choose to ignore invalid events.
- * @param event - the event to validate
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
- const txnId = InRoomChannel.getTransactionId(event);
- if (typeof txnId !== "string" || txnId.length === 0) {
- return false;
- }
- const type = InRoomChannel.getEventType(event);
- const content = event.getContent();
-
- // from here on we're fairly sure that this is supposed to be
- // part of a verification request, so be noisy when rejecting something
- if (type === REQUEST_TYPE) {
- if (!content || typeof content.to !== "string" || !content.to.length) {
- logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to));
- return false;
- }
-
- // ignore requests that are not direct to or sent by the syncing user
- if (!InRoomChannel.getOtherPartyUserId(event, client)) {
- logger.log(
- "InRoomChannel: validateEvent: " +
- `not directed to or sent by me: ${event.getSender()}` +
- `, ${content && content.to}`,
- );
- return false;
- }
- }
-
- return VerificationRequest.validateEvent(type, event, client);
- }
-
- /**
- * As m.key.verification.request events are as m.room.message events with the InRoomChannel
- * to have a fallback message in non-supporting clients, we map the real event type
- * to the symbolic one to keep things in unison with ToDeviceChannel
- * @param event - the event to get the type of
- * @returns the "symbolic" event type
- */
- public static getEventType(event: MatrixEvent): string {
- const type = event.getType();
- if (type === MESSAGE_TYPE) {
- const content = event.getContent();
- if (content) {
- const { msgtype } = content;
- if (msgtype === REQUEST_TYPE) {
- return REQUEST_TYPE;
- }
- }
- }
- if (type && type !== REQUEST_TYPE) {
- return type;
- } else {
- return "";
- }
- }
-
- /**
- * Changes the state of the channel, request, and verifier in response to a key verification event.
- * @param event - to handle
- * @param request - the request to forward handling to
- * @param isLiveEvent - whether this is an even received through sync or not
- * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
- */
- public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
- // prevent processing the same event multiple times, as under
- // some circumstances Room.timeline can get emitted twice for the same event
- if (request.hasEventId(event.getId()!)) {
- return;
- }
- const type = InRoomChannel.getEventType(event);
- // do validations that need state (roomId, userId),
- // ignore if invalid
-
- if (event.getRoomId() !== this.roomId) {
- return;
- }
- // set userId if not set already
- if (!this.userId) {
- const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
- if (userId) {
- this.userId = userId;
- }
- }
- // ignore events not sent by us or the other party
- const ownUserId = this.client.getUserId();
- const sender = event.getSender();
- if (this.userId) {
- if (sender !== ownUserId && sender !== this.userId) {
- logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`);
- return;
- }
- }
- if (!this.requestEventId) {
- this.requestEventId = InRoomChannel.getTransactionId(event);
- }
-
- const isRemoteEcho = !!event.getUnsigned().transaction_id;
- const isSentByUs = event.getSender() === this.client.getUserId();
-
- return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs);
- }
-
- /**
- * Adds the transaction id (relation) back to a received event
- * so it has the same format as returned by `completeContent` before sending.
- * The relation can not appear on the event content because of encryption,
- * relations are excluded from encryption.
- * @param event - the received event
- * @returns the content object with the relation added again
- */
- public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
- // ensure m.related_to is included in e2ee rooms
- // as the field is excluded from encryption
- const content = Object.assign({}, event.getContent());
- content[M_RELATES_TO] = event.getRelation()!;
- return content;
- }
-
- /**
- * Add all the fields to content needed for sending it over this channel.
- * This is public so verification methods (SAS uses this) can get the exact
- * content that will be sent independent of the used channel,
- * as they need to calculate the hash of it.
- * @param type - the event type
- * @param content - the (incomplete) content
- * @returns the complete content, as it will be sent.
- */
- public completeContent(type: string, content: Record<string, any>): Record<string, any> {
- content = Object.assign({}, content);
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- content.from_device = this.client.getDeviceId();
- }
- if (type === REQUEST_TYPE) {
- // type is mapped to m.room.message in the send method
- content = {
- body:
- this.client.getUserId() +
- " is requesting to verify " +
- "your key, but your client does not support in-chat key " +
- "verification. You will need to use legacy key " +
- "verification to verify keys.",
- msgtype: REQUEST_TYPE,
- to: this.userId,
- from_device: content.from_device,
- methods: content.methods,
- };
- } else {
- content[M_RELATES_TO] = {
- rel_type: M_REFERENCE,
- event_id: this.transactionId,
- };
- }
- return content;
- }
-
- /**
- * Send an event over the channel with the content not having gone through `completeContent`.
- * @param type - the event type
- * @param uncompletedContent - the (incomplete) content
- * @returns the promise of the request
- */
- public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
- const content = this.completeContent(type, uncompletedContent);
- return this.sendCompleted(type, content);
- }
-
- /**
- * Send an event over the channel with the content having gone through `completeContent` already.
- * @param type - the event type
- * @returns the promise of the request
- */
- public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
- let sendType = type;
- if (type === REQUEST_TYPE) {
- sendType = MESSAGE_TYPE;
- }
- const response = await this.client.sendEvent(this.roomId, sendType, content);
- if (type === REQUEST_TYPE) {
- this.requestEventId = response.event_id;
- }
- }
-}
-
-export class InRoomRequests implements IRequestsMap {
- private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
-
- public getRequest(event: MatrixEvent): VerificationRequest | undefined {
- const roomId = event.getRoomId()!;
- const txnId = InRoomChannel.getTransactionId(event)!;
- return this.getRequestByTxnId(roomId, txnId);
- }
-
- public getRequestByChannel(channel: InRoomChannel): VerificationRequest | undefined {
- return this.getRequestByTxnId(channel.roomId, channel.transactionId!);
- }
-
- private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest | undefined {
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- return requestsByTxnId.get(txnId);
- }
- }
-
- public setRequest(event: MatrixEvent, request: VerificationRequest): void {
- this.doSetRequest(event.getRoomId()!, InRoomChannel.getTransactionId(event)!, request);
- }
-
- public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
- this.doSetRequest(channel.roomId!, channel.transactionId!, request);
- }
-
- private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
- let requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (!requestsByTxnId) {
- requestsByTxnId = new Map();
- this.requestsByRoomId.set(roomId, requestsByTxnId);
- }
- requestsByTxnId.set(txnId, request);
- }
-
- public removeRequest(event: MatrixEvent): void {
- const roomId = event.getRoomId()!;
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- requestsByTxnId.delete(InRoomChannel.getTransactionId(event)!);
- if (requestsByTxnId.size === 0) {
- this.requestsByRoomId.delete(roomId);
- }
- }
- }
-
- public findRequestInProgress(roomId: string): VerificationRequest | undefined {
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- for (const request of requestsByTxnId.values()) {
- if (request.pending) {
- return request;
- }
- }
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
deleted file mode 100644
index d51b85a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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 { randomString } from "../../../randomstring";
-import { logger } from "../../../logger";
-import {
- CANCEL_TYPE,
- PHASE_STARTED,
- PHASE_READY,
- REQUEST_TYPE,
- READY_TYPE,
- START_TYPE,
- VerificationRequest,
-} from "./VerificationRequest";
-import { errorFromEvent, newUnexpectedMessageError } from "../Error";
-import { MatrixEvent } from "../../../models/event";
-import { IVerificationChannel } from "./Channel";
-import { MatrixClient } from "../../../client";
-import { IRequestsMap } from "../..";
-
-export type Request = VerificationRequest<ToDeviceChannel>;
-
-/**
- * A key verification channel that sends verification events over to_device messages.
- * Generates its own transaction ids.
- */
-export class ToDeviceChannel implements IVerificationChannel {
- public request?: VerificationRequest;
-
- // userId and devices of user we're about to verify
- public constructor(
- private readonly client: MatrixClient,
- public readonly userId: string,
- private readonly devices: string[],
- public transactionId?: string,
- public deviceId?: string,
- ) {}
-
- public isToDevices(devices: string[]): boolean {
- if (devices.length === this.devices.length) {
- for (const device of devices) {
- if (!this.devices.includes(device)) {
- return false;
- }
- }
- return true;
- } else {
- return false;
- }
- }
-
- public static getEventType(event: MatrixEvent): string {
- return event.getType();
- }
-
- /**
- * Extract the transaction id used by a given key verification event, if any
- * @param event - the event
- * @returns the transaction id
- */
- public static getTransactionId(event: MatrixEvent): string {
- const content = event.getContent();
- return content && content.transaction_id;
- }
-
- /**
- * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
- * @param type - the event type to check
- * @returns boolean flag
- */
- public static canCreateRequest(type: string): boolean {
- return type === REQUEST_TYPE || type === START_TYPE;
- }
-
- public canCreateRequest(type: string): boolean {
- return ToDeviceChannel.canCreateRequest(type);
- }
-
- /**
- * Checks whether this event is a well-formed key verification event.
- * This only does checks that don't rely on the current state of a potentially already channel
- * so we can prevent channels being created by invalid events.
- * `handleEvent` can do more checks and choose to ignore invalid events.
- * @param event - the event to validate
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
- if (event.isCancelled()) {
- logger.warn("Ignoring flagged verification request from " + event.getSender());
- return false;
- }
- const content = event.getContent();
- if (!content) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
- return false;
- }
-
- if (!content.transaction_id) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
- return false;
- }
-
- const type = event.getType();
-
- if (type === REQUEST_TYPE) {
- if (!Number.isFinite(content.timestamp)) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
- return false;
- }
- if (event.getSender() === client.getUserId() && content.from_device == client.getDeviceId()) {
- // ignore requests from ourselves, because it doesn't make sense for a
- // device to verify itself
- logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
- return false;
- }
- }
-
- return VerificationRequest.validateEvent(type, event, client);
- }
-
- /**
- * @param event - the event to get the timestamp of
- * @returns the timestamp when the event was sent
- */
- public getTimestamp(event: MatrixEvent): number {
- const content = event.getContent();
- return content && content.timestamp;
- }
-
- /**
- * Changes the state of the channel, request, and verifier in response to a key verification event.
- * @param event - to handle
- * @param request - the request to forward handling to
- * @param isLiveEvent - whether this is an even received through sync or not
- * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
- */
- public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
- const type = event.getType();
- const content = event.getContent();
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- if (!this.transactionId) {
- this.transactionId = content.transaction_id;
- }
- const deviceId = content.from_device;
- // adopt deviceId if not set before and valid
- if (!this.deviceId && this.devices.includes(deviceId)) {
- this.deviceId = deviceId;
- }
- // if no device id or different from adopted one, cancel with sender
- if (!this.deviceId || this.deviceId !== deviceId) {
- // also check that message came from the device we sent the request to earlier on
- // and do send a cancel message to that device
- // (but don't cancel the request for the device we should be talking to)
- const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
- return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
- }
- }
- const wasStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
-
- await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
-
- const isStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
-
- const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
- // the request has picked a ready or start event, tell the other devices about it
- if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
- const nonChosenDevices = this.devices.filter((d) => d !== this.deviceId && d !== this.client.getDeviceId());
- if (nonChosenDevices.length) {
- const message = this.completeContent(CANCEL_TYPE, {
- code: "m.accepted",
- reason: "Verification request accepted by another device",
- });
- await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
- }
- }
- }
-
- /**
- * See {@link InRoomChannel#completedContentFromEvent} for why this is needed.
- * @param event - the received event
- * @returns the content object
- */
- public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
- return event.getContent();
- }
-
- /**
- * Add all the fields to content needed for sending it over this channel.
- * This is public so verification methods (SAS uses this) can get the exact
- * content that will be sent independent of the used channel,
- * as they need to calculate the hash of it.
- * @param type - the event type
- * @param content - the (incomplete) content
- * @returns the complete content, as it will be sent.
- */
- public completeContent(type: string, content: Record<string, any>): Record<string, any> {
- // make a copy
- content = Object.assign({}, content);
- if (this.transactionId) {
- content.transaction_id = this.transactionId;
- }
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- content.from_device = this.client.getDeviceId();
- }
- if (type === REQUEST_TYPE) {
- content.timestamp = Date.now();
- }
- return content;
- }
-
- /**
- * Send an event over the channel with the content not having gone through `completeContent`.
- * @param type - the event type
- * @param uncompletedContent - the (incomplete) content
- * @returns the promise of the request
- */
- public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
- // create transaction id when sending request
- if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
- this.transactionId = ToDeviceChannel.makeTransactionId();
- }
- const content = this.completeContent(type, uncompletedContent);
- return this.sendCompleted(type, content);
- }
-
- /**
- * Send an event over the channel with the content having gone through `completeContent` already.
- * @param type - the event type
- * @returns the promise of the request
- */
- public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
- let result;
- if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
- result = await this.sendToDevices(type, content, this.devices);
- } else {
- result = await this.sendToDevices(type, content, [this.deviceId!]);
- }
- // the VerificationRequest state machine requires remote echos of the event
- // the client sends itself, so we fake this for to_device messages
- const remoteEchoEvent = new MatrixEvent({
- sender: this.client.getUserId()!,
- content,
- type,
- });
- await this.request!.handleEvent(
- type,
- remoteEchoEvent,
- /*isLiveEvent=*/ true,
- /*isRemoteEcho=*/ true,
- /*isSentByUs=*/ true,
- );
- return result;
- }
-
- private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
- if (devices.length) {
- const deviceMessages: Map<string, Record<string, any>> = new Map();
- for (const deviceId of devices) {
- deviceMessages.set(deviceId, content);
- }
-
- await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]]));
- }
- }
-
- /**
- * Allow Crypto module to create and know the transaction id before the .start event gets sent.
- * @returns the transaction id
- */
- public static makeTransactionId(): string {
- return randomString(32);
- }
-}
-
-export class ToDeviceRequests implements IRequestsMap {
- private requestsByUserId = new Map<string, Map<string, Request>>();
-
- public getRequest(event: MatrixEvent): Request | undefined {
- return this.getRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event));
- }
-
- public getRequestByChannel(channel: ToDeviceChannel): Request | undefined {
- return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId!);
- }
-
- public getRequestBySenderAndTxnId(sender: string, txnId: string): Request | undefined {
- const requestsByTxnId = this.requestsByUserId.get(sender);
- if (requestsByTxnId) {
- return requestsByTxnId.get(txnId);
- }
- }
-
- public setRequest(event: MatrixEvent, request: Request): void {
- this.setRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event), request);
- }
-
- public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
- this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId!, request);
- }
-
- public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
- let requestsByTxnId = this.requestsByUserId.get(sender);
- if (!requestsByTxnId) {
- requestsByTxnId = new Map();
- this.requestsByUserId.set(sender, requestsByTxnId);
- }
- requestsByTxnId.set(txnId, request);
- }
-
- public removeRequest(event: MatrixEvent): void {
- const userId = event.getSender()!;
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
- if (requestsByTxnId.size === 0) {
- this.requestsByUserId.delete(userId);
- }
- }
- }
-
- public findRequestInProgress(userId: string, devices: string[]): Request | undefined {
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- for (const request of requestsByTxnId.values()) {
- if (request.pending && request.channel.isToDevices(devices)) {
- return request;
- }
- }
- }
- }
-
- public getRequestsInProgress(userId: string): Request[] {
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- return Array.from(requestsByTxnId.values()).filter((r) => r.pending);
- }
- return [];
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
deleted file mode 100644
index 617432e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
+++ /dev/null
@@ -1,926 +0,0 @@
-/*
-Copyright 2018 - 2021 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 { logger } from "../../../logger";
-import { errorFactory, errorFromEvent, newUnexpectedMessageError, newUnknownMethodError } from "../Error";
-import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
-import { IVerificationChannel } from "./Channel";
-import { MatrixClient } from "../../../client";
-import { MatrixEvent } from "../../../models/event";
-import { EventType } from "../../../@types/event";
-import { VerificationBase } from "../Base";
-import { VerificationMethod } from "../../index";
-import { TypedEventEmitter } from "../../../models/typed-event-emitter";
-
-// How long after the event's timestamp that the request times out
-const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
-
-// How long after we receive the event that the request times out
-const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
-
-// to avoid almost expired verification notifications
-// from showing a notification and almost immediately
-// disappearing, also ignore verification requests that
-// are this amount of time away from expiring.
-const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
-
-export const EVENT_PREFIX = "m.key.verification.";
-export const REQUEST_TYPE = EVENT_PREFIX + "request";
-export const START_TYPE = EVENT_PREFIX + "start";
-export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
-export const DONE_TYPE = EVENT_PREFIX + "done";
-export const READY_TYPE = EVENT_PREFIX + "ready";
-
-export enum Phase {
- Unsent = 1,
- Requested,
- Ready,
- Started,
- Cancelled,
- Done,
-}
-
-// Legacy export fields
-export const PHASE_UNSENT = Phase.Unsent;
-export const PHASE_REQUESTED = Phase.Requested;
-export const PHASE_READY = Phase.Ready;
-export const PHASE_STARTED = Phase.Started;
-export const PHASE_CANCELLED = Phase.Cancelled;
-export const PHASE_DONE = Phase.Done;
-
-interface ITargetDevice {
- userId?: string;
- deviceId?: string;
-}
-
-interface ITransition {
- phase: Phase;
- event?: MatrixEvent;
-}
-
-export enum VerificationRequestEvent {
- Change = "change",
-}
-
-type EventHandlerMap = {
- /**
- * Fires whenever the state of the request object has changed.
- */
- [VerificationRequestEvent.Change]: () => void;
-};
-
-/**
- * State machine for verification requests.
- * Things that differ based on what channel is used to
- * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
- */
-export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends TypedEventEmitter<
- VerificationRequestEvent,
- EventHandlerMap
-> {
- private eventsByUs = new Map<string, MatrixEvent>();
- private eventsByThem = new Map<string, MatrixEvent>();
- private _observeOnly = false;
- private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
- private _accepting = false;
- private _declining = false;
- private verifierHasFinished = false;
- private _cancelled = false;
- private _chosenMethod: VerificationMethod | null = null;
- // we keep a copy of the QR Code data (including other user master key) around
- // for QR reciprocate verification, to protect against
- // cross-signing identity reset between the .ready and .start event
- // and signing the wrong key after .start
- private _qrCodeData: QRCodeData | null = null;
-
- // The timestamp when we received the request event from the other side
- private requestReceivedAt: number | null = null;
-
- private commonMethods: VerificationMethod[] = [];
- private _phase!: Phase;
- public _cancellingUserId?: string; // Used in tests only
- private _verifier?: VerificationBase<any, any>;
-
- public constructor(
- public readonly channel: C,
- private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
- private readonly client: MatrixClient,
- ) {
- super();
- this.channel.request = this;
- this.setPhase(PHASE_UNSENT, false);
- }
-
- /**
- * Stateless validation logic not specific to the channel.
- * Invoked by the same static method in either channel.
- * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
- * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead.
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
- const content = event.getContent();
-
- if (!type || !type.startsWith(EVENT_PREFIX)) {
- return false;
- }
-
- // from here on we're fairly sure that this is supposed to be
- // part of a verification request, so be noisy when rejecting something
- if (!content) {
- logger.log("VerificationRequest: validateEvent: no content");
- return false;
- }
-
- if (type === REQUEST_TYPE || type === READY_TYPE) {
- if (!Array.isArray(content.methods)) {
- logger.log("VerificationRequest: validateEvent: " + "fail because methods");
- return false;
- }
- }
-
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- if (typeof content.from_device !== "string" || content.from_device.length === 0) {
- logger.log("VerificationRequest: validateEvent: " + "fail because from_device");
- return false;
- }
- }
-
- return true;
- }
-
- public get invalid(): boolean {
- return this.phase === PHASE_UNSENT;
- }
-
- /** returns whether the phase is PHASE_REQUESTED */
- public get requested(): boolean {
- return this.phase === PHASE_REQUESTED;
- }
-
- /** returns whether the phase is PHASE_CANCELLED */
- public get cancelled(): boolean {
- return this.phase === PHASE_CANCELLED;
- }
-
- /** returns whether the phase is PHASE_READY */
- public get ready(): boolean {
- return this.phase === PHASE_READY;
- }
-
- /** returns whether the phase is PHASE_STARTED */
- public get started(): boolean {
- return this.phase === PHASE_STARTED;
- }
-
- /** returns whether the phase is PHASE_DONE */
- public get done(): boolean {
- return this.phase === PHASE_DONE;
- }
-
- /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
- public get methods(): VerificationMethod[] {
- return this.commonMethods;
- }
-
- /** the method picked in the .start event */
- public get chosenMethod(): VerificationMethod | null {
- return this._chosenMethod;
- }
-
- public calculateEventTimeout(event: MatrixEvent): number {
- let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS;
-
- if (this.requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED) {
- const expiresAtByReceipt = this.requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT;
- effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
- }
-
- return Math.max(0, effectiveExpiresAt - Date.now());
- }
-
- /** The current remaining amount of ms before the request should be automatically cancelled */
- public get timeout(): number {
- const requestEvent = this.getEventByEither(REQUEST_TYPE);
- if (requestEvent) {
- return this.calculateEventTimeout(requestEvent);
- }
- return 0;
- }
-
- /**
- * The key verification request event.
- * @returns The request event, or falsey if not found.
- */
- public get requestEvent(): MatrixEvent | undefined {
- return this.getEventByEither(REQUEST_TYPE);
- }
-
- /** current phase of the request. Some properties might only be defined in a current phase. */
- public get phase(): Phase {
- return this._phase;
- }
-
- /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
- public get verifier(): VerificationBase<any, any> | undefined {
- return this._verifier;
- }
-
- public get canAccept(): boolean {
- return this.phase < PHASE_READY && !this._accepting && !this._declining;
- }
-
- public get accepting(): boolean {
- return this._accepting;
- }
-
- public get declining(): boolean {
- return this._declining;
- }
-
- /** whether this request has sent it's initial event and needs more events to complete */
- public get pending(): boolean {
- return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED;
- }
-
- /** Only set after a .ready if the other party can scan a QR code */
- public get qrCodeData(): QRCodeData | null {
- return this._qrCodeData;
- }
-
- /** Checks whether the other party supports a given verification method.
- * This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
- * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
- * For methods that need to be supported by both ends, use the `methods` property.
- * @param method - the method to check
- * @param force - to check even if the phase is not ready or started yet, internal usage
- * @returns whether or not the other party said the supported the method */
- public otherPartySupportsMethod(method: string, force = false): boolean {
- if (!force && !this.ready && !this.started) {
- return false;
- }
- const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(READY_TYPE);
- if (!theirMethodEvent) {
- // if we started straight away with .start event,
- // we are assuming that the other side will support the
- // chosen method, so return true for that.
- if (this.started && this.initiatedByMe) {
- const myStartEvent = this.eventsByUs.get(START_TYPE);
- const content = myStartEvent && myStartEvent.getContent();
- const myStartMethod = content && content.method;
- return method == myStartMethod;
- }
- return false;
- }
- const content = theirMethodEvent.getContent();
- if (!content) {
- return false;
- }
- const { methods } = content;
- if (!Array.isArray(methods)) {
- return false;
- }
-
- return methods.includes(method);
- }
-
- /** Whether this request was initiated by the syncing user.
- * For InRoomChannel, this is who sent the .request event.
- * For ToDeviceChannel, this is who sent the .start event
- */
- public get initiatedByMe(): boolean {
- // event created by us but no remote echo has been received yet
- const noEventsYet = this.eventsByUs.size + this.eventsByThem.size === 0;
- if (this._phase === PHASE_UNSENT && noEventsYet) {
- return true;
- }
- const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
- const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
- if (hasMyRequest && !hasTheirRequest) {
- return true;
- }
- if (!hasMyRequest && hasTheirRequest) {
- return false;
- }
- const hasMyStart = this.eventsByUs.has(START_TYPE);
- const hasTheirStart = this.eventsByThem.has(START_TYPE);
- if (hasMyStart && !hasTheirStart) {
- return true;
- }
- return false;
- }
-
- /** The id of the user that initiated the request */
- public get requestingUserId(): string {
- if (this.initiatedByMe) {
- return this.client.getUserId()!;
- } else {
- return this.otherUserId;
- }
- }
-
- /** The id of the user that (will) receive(d) the request */
- public get receivingUserId(): string {
- if (this.initiatedByMe) {
- return this.otherUserId;
- } else {
- return this.client.getUserId()!;
- }
- }
-
- /** The user id of the other party in this request */
- public get otherUserId(): string {
- return this.channel.userId!;
- }
-
- public get isSelfVerification(): boolean {
- return this.client.getUserId() === this.otherUserId;
- }
-
- /**
- * The id of the user that cancelled the request,
- * only defined when phase is PHASE_CANCELLED
- */
- public get cancellingUserId(): string | undefined {
- const myCancel = this.eventsByUs.get(CANCEL_TYPE);
- const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
-
- if (myCancel && (!theirCancel || myCancel.getId()! < theirCancel.getId()!)) {
- return myCancel.getSender();
- }
- if (theirCancel) {
- return theirCancel.getSender();
- }
- return undefined;
- }
-
- /**
- * The cancellation code e.g m.user which is responsible for cancelling this verification
- */
- public get cancellationCode(): string {
- const ev = this.getEventByEither(CANCEL_TYPE);
- return ev ? ev.getContent().code : null;
- }
-
- public get observeOnly(): boolean {
- return this._observeOnly;
- }
-
- /**
- * Gets which device the verification should be started with
- * given the events sent so far in the verification. This is the
- * same algorithm used to determine which device to send the
- * verification to when no specific device is specified.
- * @returns The device information
- */
- public get targetDevice(): ITargetDevice {
- const theirFirstEvent =
- this.eventsByThem.get(REQUEST_TYPE) ||
- this.eventsByThem.get(READY_TYPE) ||
- this.eventsByThem.get(START_TYPE);
- const theirFirstContent = theirFirstEvent?.getContent();
- const fromDevice = theirFirstContent?.from_device;
- return {
- userId: this.otherUserId,
- deviceId: fromDevice,
- };
- }
-
- /* Start the key verification, creating a verifier and sending a .start event.
- * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
- * @param method - the name of the verification method to use.
- * @param targetDevice.userId the id of the user to direct this request to
- * @param targetDevice.deviceId the id of the device to direct this request to
- * @returns the verifier of the given method
- */
- public beginKeyVerification(
- method: VerificationMethod,
- targetDevice: ITargetDevice | null = null,
- ): VerificationBase<any, any> {
- // need to allow also when unsent in case of to_device
- if (!this.observeOnly && !this._verifier) {
- const validStartPhase =
- this.phase === PHASE_REQUESTED ||
- this.phase === PHASE_READY ||
- (this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
- if (validStartPhase) {
- // when called on a request that was initiated with .request event
- // check the method is supported by both sides
- if (this.commonMethods.length && !this.commonMethods.includes(method)) {
- throw newUnknownMethodError();
- }
- this._verifier = this.createVerifier(method, null, targetDevice);
- if (!this._verifier) {
- throw newUnknownMethodError();
- }
- this._chosenMethod = method;
- }
- }
- return this._verifier!;
- }
-
- /**
- * sends the initial .request event.
- * @returns resolves when the event has been sent.
- */
- public async sendRequest(): Promise<void> {
- if (!this.observeOnly && this._phase === PHASE_UNSENT) {
- const methods = [...this.verificationMethods.keys()];
- await this.channel.send(REQUEST_TYPE, { methods });
- }
- }
-
- /**
- * Cancels the request, sending a cancellation to the other party
- * @param reason - the error reason to send the cancellation with
- * @param code - the error code to send the cancellation with
- * @returns resolves when the event has been sent.
- */
- public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
- if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
- this._declining = true;
- this.emit(VerificationRequestEvent.Change);
- if (this._verifier) {
- return this._verifier.cancel(errorFactory(code, reason)());
- } else {
- this._cancellingUserId = this.client.getUserId()!;
- await this.channel.send(CANCEL_TYPE, { code, reason });
- }
- }
- }
-
- /**
- * Accepts the request, sending a .ready event to the other party
- * @returns resolves when the event has been sent.
- */
- public async accept(): Promise<void> {
- if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
- const methods = [...this.verificationMethods.keys()];
- this._accepting = true;
- this.emit(VerificationRequestEvent.Change);
- await this.channel.send(READY_TYPE, { methods });
- }
- }
-
- /**
- * Can be used to listen for state changes until the callback returns true.
- * @param fn - callback to evaluate whether the request is in the desired state.
- * Takes the request as an argument.
- * @returns that resolves once the callback returns true
- * @throws Error when the request is cancelled
- */
- public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
- return new Promise((resolve, reject) => {
- const check = (): boolean => {
- let handled = false;
- if (fn(this)) {
- resolve(this);
- handled = true;
- } else if (this.cancelled) {
- reject(new Error("cancelled"));
- handled = true;
- }
- if (handled) {
- this.off(VerificationRequestEvent.Change, check);
- }
- return handled;
- };
- if (!check()) {
- this.on(VerificationRequestEvent.Change, check);
- }
- });
- }
-
- private setPhase(phase: Phase, notify = true): void {
- this._phase = phase;
- if (notify) {
- this.emit(VerificationRequestEvent.Change);
- }
- }
-
- private getEventByEither(type: string): MatrixEvent | undefined {
- return this.eventsByThem.get(type) || this.eventsByUs.get(type);
- }
-
- private getEventBy(type: string, byThem = false): MatrixEvent | undefined {
- if (byThem) {
- return this.eventsByThem.get(type);
- } else {
- return this.eventsByUs.get(type);
- }
- }
-
- private calculatePhaseTransitions(): ITransition[] {
- const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
- const phase = (): Phase => transitions[transitions.length - 1].phase;
-
- // always pass by .request first to be sure channel.userId has been set
- const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
- const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
- if (requestEvent) {
- transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
- }
-
- const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
- if (readyEvent && phase() === PHASE_REQUESTED) {
- transitions.push({ phase: PHASE_READY, event: readyEvent });
- }
-
- let startEvent: MatrixEvent | undefined;
- if (readyEvent || !requestEvent) {
- const theirStartEvent = this.eventsByThem.get(START_TYPE);
- const ourStartEvent = this.eventsByUs.get(START_TYPE);
- // any party can send .start after a .ready or unsent
- if (theirStartEvent && ourStartEvent) {
- startEvent =
- theirStartEvent.getSender()! < ourStartEvent.getSender()! ? theirStartEvent : ourStartEvent;
- } else {
- startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
- }
- } else {
- startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
- }
- if (startEvent) {
- const fromRequestPhase =
- phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender();
- const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
- if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
- transitions.push({ phase: PHASE_STARTED, event: startEvent });
- }
- }
-
- const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
- if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
- transitions.push({ phase: PHASE_DONE });
- }
-
- const cancelEvent = this.getEventByEither(CANCEL_TYPE);
- if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
- transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
- return transitions;
- }
-
- return transitions;
- }
-
- private transitionToPhase(transition: ITransition): void {
- const { phase, event } = transition;
- // get common methods
- if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
- if (!this.wasSentByOwnDevice(event)) {
- const content = event!.getContent<{
- methods: string[];
- }>();
- this.commonMethods = content.methods.filter((m) => this.verificationMethods.has(m));
- }
- }
- // detect if we're not a party in the request, and we should just observe
- if (!this.observeOnly) {
- // if requested or accepted by one of my other devices
- if (phase === PHASE_REQUESTED || phase === PHASE_STARTED || phase === PHASE_READY) {
- if (
- this.channel.receiveStartFromOtherDevices &&
- this.wasSentByOwnUser(event) &&
- !this.wasSentByOwnDevice(event)
- ) {
- this._observeOnly = true;
- }
- }
- }
- // create verifier
- if (phase === PHASE_STARTED) {
- const { method } = event!.getContent();
- if (!this._verifier && !this.observeOnly) {
- this._verifier = this.createVerifier(method, event);
- if (!this._verifier) {
- this.cancel({
- code: "m.unknown_method",
- reason: `Unknown method: ${method}`,
- });
- } else {
- this._chosenMethod = method;
- }
- }
- }
- }
-
- private applyPhaseTransitions(): ITransition[] {
- const transitions = this.calculatePhaseTransitions();
- const existingIdx = transitions.findIndex((t) => t.phase === this.phase);
- // trim off phases we already went through, if any
- const newTransitions = transitions.slice(existingIdx + 1);
- // transition to all new phases
- for (const transition of newTransitions) {
- this.transitionToPhase(transition);
- }
- return newTransitions;
- }
-
- private isWinningStartRace(newEvent: MatrixEvent): boolean {
- if (newEvent.getType() !== START_TYPE) {
- return false;
- }
- const oldEvent = this._verifier!.startEvent;
-
- let oldRaceIdentifier;
- if (this.isSelfVerification) {
- // if the verifier does not have a startEvent,
- // it is because it's still sending and we are on the initator side
- // we know we are sending a .start event because we already
- // have a verifier (checked in calling method)
- if (oldEvent) {
- const oldContent = oldEvent.getContent();
- oldRaceIdentifier = oldContent && oldContent.from_device;
- } else {
- oldRaceIdentifier = this.client.getDeviceId();
- }
- } else {
- if (oldEvent) {
- oldRaceIdentifier = oldEvent.getSender();
- } else {
- oldRaceIdentifier = this.client.getUserId();
- }
- }
-
- let newRaceIdentifier;
- if (this.isSelfVerification) {
- const newContent = newEvent.getContent();
- newRaceIdentifier = newContent && newContent.from_device;
- } else {
- newRaceIdentifier = newEvent.getSender();
- }
- return newRaceIdentifier < oldRaceIdentifier;
- }
-
- public hasEventId(eventId: string): boolean {
- for (const event of this.eventsByUs.values()) {
- if (event.getId() === eventId) {
- return true;
- }
- }
- for (const event of this.eventsByThem.values()) {
- if (event.getId() === eventId) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Changes the state of the request and verifier in response to a key verification event.
- * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
- * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead.
- * @param isLiveEvent - whether this is an even received through sync or not
- * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device
- * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers.
- * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
- * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
- */
- public async handleEvent(
- type: string,
- event: MatrixEvent,
- isLiveEvent: boolean,
- isRemoteEcho: boolean,
- isSentByUs: boolean,
- ): Promise<void> {
- // if reached phase cancelled or done, ignore anything else that comes
- if (this.done || this.cancelled) {
- return;
- }
- const wasObserveOnly = this._observeOnly;
-
- this.adjustObserveOnly(event, isLiveEvent);
-
- if (!this.observeOnly && !isRemoteEcho) {
- if (await this.cancelOnError(type, event)) {
- return;
- }
- }
-
- // This assumes verification won't need to send an event with
- // the same type for the same party twice.
- // This is true for QR and SAS verification, and was
- // added here to prevent verification getting cancelled
- // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
- const isDuplicateEvent = isSentByUs ? this.eventsByUs.has(type) : this.eventsByThem.has(type);
- if (isDuplicateEvent) {
- return;
- }
-
- const oldPhase = this.phase;
- this.addEvent(type, event, isSentByUs);
-
- // this will create if needed the verifier so needs to happen before calling it
- const newTransitions = this.applyPhaseTransitions();
- try {
- // only pass events from the other side to the verifier,
- // no remote echos of our own events
- if (this._verifier && !this.observeOnly) {
- const newEventWinsRace = this.isWinningStartRace(event);
- if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
- this._verifier.switchStartEvent(event);
- } else if (!isRemoteEcho) {
- if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
- this._verifier.handleEvent(event);
- }
- }
- }
-
- if (newTransitions.length) {
- // create QRCodeData if the other side can scan
- // important this happens before emitting a phase change,
- // so listeners can rely on it being there already
- // We only do this for live events because it is important that
- // we sign the keys that were in the QR code, and not the keys
- // we happen to have at some later point in time.
- if (isLiveEvent && newTransitions.some((t) => t.phase === PHASE_READY)) {
- const shouldGenerateQrCode = this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
- if (shouldGenerateQrCode) {
- this._qrCodeData = await QRCodeData.create(this, this.client);
- }
- }
-
- const lastTransition = newTransitions[newTransitions.length - 1];
- const { phase } = lastTransition;
-
- this.setupTimeout(phase);
- // set phase as last thing as this emits the "change" event
- this.setPhase(phase);
- } else if (this._observeOnly !== wasObserveOnly) {
- this.emit(VerificationRequestEvent.Change);
- }
- } finally {
- // log events we processed so we can see from rageshakes what events were added to a request
- logger.log(
- `Verification request ${this.channel.transactionId}: ` +
- `${type} event with id:${event.getId()}, ` +
- `content:${JSON.stringify(event.getContent())} ` +
- `deviceId:${this.channel.deviceId}, ` +
- `sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
- `isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
- `phase:${oldPhase}=>${this.phase}, ` +
- `observeOnly:${wasObserveOnly}=>${this._observeOnly}`,
- );
- }
- }
-
- private setupTimeout(phase: Phase): void {
- const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
-
- if (shouldTimeout) {
- this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
- }
- if (this.timeoutTimer) {
- const shouldClear =
- phase === PHASE_STARTED || phase === PHASE_READY || phase === PHASE_DONE || phase === PHASE_CANCELLED;
- if (shouldClear) {
- clearTimeout(this.timeoutTimer);
- this.timeoutTimer = null;
- }
- }
- }
-
- private cancelOnTimeout = async (): Promise<void> => {
- try {
- if (this.initiatedByMe) {
- await this.cancel({
- reason: "Other party didn't accept in time",
- code: "m.timeout",
- });
- } else {
- await this.cancel({
- reason: "User didn't accept in time",
- code: "m.timeout",
- });
- }
- } catch (err) {
- logger.error("Error while cancelling verification request", err);
- }
- };
-
- private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
- if (type === START_TYPE) {
- const method = event.getContent().method;
- if (!this.verificationMethods.has(method)) {
- await this.cancel(errorFromEvent(newUnknownMethodError()));
- return true;
- }
- }
-
- const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
- const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED;
- // only if phase has passed from PHASE_UNSENT should we cancel, because events
- // are allowed to come in in any order (at least with InRoomChannel). So we only know
- // we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED.
- // Before that, we could be looking at somebody else's verification request and we just
- // happen to be in the room
- if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
- logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`);
- const reason = `Unexpected ${type} event in phase ${this.phase}`;
- await this.cancel(errorFromEvent(newUnexpectedMessageError({ reason })));
- return true;
- }
- return false;
- }
-
- private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
- // don't send out events for historical requests
- if (!isLiveEvent) {
- this._observeOnly = true;
- }
- if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
- this._observeOnly = true;
- }
- }
-
- private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
- if (isSentByUs) {
- this.eventsByUs.set(type, event);
- } else {
- this.eventsByThem.set(type, event);
- }
-
- // once we know the userId of the other party (from the .request event)
- // see if any event by anyone else crept into this.eventsByThem
- if (type === REQUEST_TYPE) {
- for (const [type, event] of this.eventsByThem.entries()) {
- if (event.getSender() !== this.otherUserId) {
- this.eventsByThem.delete(type);
- }
- }
- // also remember when we received the request event
- this.requestReceivedAt = Date.now();
- }
- }
-
- private createVerifier(
- method: VerificationMethod,
- startEvent: MatrixEvent | null = null,
- targetDevice: ITargetDevice | null = null,
- ): VerificationBase<any, any> | undefined {
- if (!targetDevice) {
- targetDevice = this.targetDevice;
- }
- const { userId, deviceId } = targetDevice;
-
- const VerifierCtor = this.verificationMethods.get(method);
- if (!VerifierCtor) {
- logger.warn("could not find verifier constructor for method", method);
- return;
- }
- return new VerifierCtor(this.channel, this.client, userId!, deviceId!, startEvent, this);
- }
-
- private wasSentByOwnUser(event?: MatrixEvent): boolean {
- return event?.getSender() === this.client.getUserId();
- }
-
- // only for .request, .ready or .start
- private wasSentByOwnDevice(event?: MatrixEvent): boolean {
- if (!this.wasSentByOwnUser(event)) {
- return false;
- }
- const content = event!.getContent();
- if (!content || content.from_device !== this.client.getDeviceId()) {
- return false;
- }
- return true;
- }
-
- public onVerifierCancelled(): void {
- this._cancelled = true;
- // move to cancelled phase
- const newTransitions = this.applyPhaseTransitions();
- if (newTransitions.length) {
- this.setPhase(newTransitions[newTransitions.length - 1].phase);
- }
- }
-
- public onVerifierFinished(): void {
- this.channel.send(EventType.KeyVerificationDone, {});
- this.verifierHasFinished = true;
- // move to .done phase
- const newTransitions = this.applyPhaseTransitions();
- if (newTransitions.length) {
- this.setPhase(newTransitions[newTransitions.length - 1].phase);
- }
- }
-
- public getEventFromOtherParty(type: string): MatrixEvent | undefined {
- return this.eventsByThem.get(type);
- }
-}