summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/crypto')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js703
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js860
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js342
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js1162
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js406
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js60
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretSharing.js199
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js119
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js127
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js226
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js18
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js1682
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js276
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js12
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js651
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js60
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js237
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/device-converter.js47
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js152
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js3427
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js69
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/keybackup.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js480
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js60
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/base.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js913
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js599
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js329
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js439
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js345
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js100
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js46
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js269
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js454
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js39
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/Channel.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js349
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js322
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js870
39 files changed, 16464 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js
new file mode 100644
index 0000000000..be8c9607f4
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js
@@ -0,0 +1,703 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.UserTrustLevel = exports.DeviceTrustLevel = exports.CrossSigningLevel = exports.CrossSigningInfo = void 0;
+exports.createCryptoStoreCacheCallbacks = createCryptoStoreCacheCallbacks;
+exports.requestKeysDuringVerification = requestKeysDuringVerification;
+var _olmlib = require("./olmlib");
+var _logger = require("../logger");
+var _indexeddbCryptoStore = require("../crypto/store/indexeddb-crypto-store");
+var _aes = require("./aes");
+var _cryptoApi = require("../crypto-api");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
+function publicKeyFromKeyInfo(keyInfo) {
+ // `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];
+}
+class CrossSigningInfo {
+ /**
+ * 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
+ */
+ constructor(userId, callbacks = {}, cacheCallbacks = {}) {
+ this.userId = userId;
+ this.callbacks = callbacks;
+ this.cacheCallbacks = cacheCallbacks;
+ _defineProperty(this, "keys", {});
+ _defineProperty(this, "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.
+ _defineProperty(this, "crossSigningVerifiedBefore", false);
+ }
+ static fromStorage(obj, userId) {
+ 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;
+ }
+ toStorage() {
+ 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 ]
+ */
+ async getCrossSigningKey(type, expectedPubkey) {
+ 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) {
+ 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 = 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
+ */
+ async isStoredInSecretStorage(secretStorage) {
+ // 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) {
+ 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
+ */
+ static async storeInSecretStorage(keys, secretStorage) {
+ for (const [type, privateKey] of keys) {
+ const encodedKey = (0, _olmlib.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
+ */
+ static async getFromSecretStorage(type, secretStorage) {
+ const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
+ if (!encodedKey) {
+ return null;
+ }
+ return (0, _olmlib.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.
+ */
+ async isStoredInKeyCache(type) {
+ 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)
+ */
+ async getCrossSigningKeysFromCache() {
+ 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
+ */
+ getId(type = "master") {
+ 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
+ */
+ async resetKeys(level) {
+ 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) {
+ return;
+ }
+ const privateKeys = {};
+ const keys = {};
+ 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
+ }
+ };
+ (0, _olmlib.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
+ }
+ };
+ (0, _olmlib.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
+ */
+ clearKeys() {
+ this.keys = {};
+ }
+ setKeys(keys) {
+ const signingKeys = {};
+ 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.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.logger.error(error);
+ throw new Error(error);
+ }
+ try {
+ (0, _olmlib.pkVerify)(keys.user_signing, masterKey, this.userId);
+ } catch (e) {
+ _logger.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.logger.error(error);
+ throw new Error(error);
+ }
+ try {
+ (0, _olmlib.pkVerify)(keys.self_signing, masterKey, this.userId);
+ } catch (e) {
+ _logger.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;
+ }
+ }
+ updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
+ // 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;
+ }
+ }
+ async signObject(data, type) {
+ 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 {
+ (0, _olmlib.pkSign)(data, signing, this.userId, pubkey);
+ return data;
+ } finally {
+ signing.free();
+ }
+ }
+ async signUser(key) {
+ if (!this.keys.user_signing) {
+ _logger.logger.info("No user signing key: not signing user");
+ return;
+ }
+ return this.signObject(key.keys.master, "user_signing");
+ }
+ async signDevice(userId, device) {
+ 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.logger.info("No self signing key: not signing device");
+ return;
+ }
+ return this.signObject({
+ 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
+ */
+ checkUserTrust(userCrossSigning) {
+ // 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;
+ const userMaster = userCrossSigning.keys.master;
+ const uskId = this.getId("user_signing");
+ try {
+ (0, _olmlib.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
+ */
+ checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
+ 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...
+ (0, _olmlib.pkVerify)(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
+ // ...and this device's key from their SSK...
+ (0, _olmlib.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
+ */
+ getCacheCallbacks() {
+ return this.cacheCallbacks;
+ }
+}
+exports.CrossSigningInfo = CrossSigningInfo;
+function deviceToObject(device, userId) {
+ return {
+ algorithms: device.algorithms,
+ keys: device.keys,
+ device_id: device.deviceId,
+ user_id: userId,
+ signatures: device.signatures
+ };
+}
+let CrossSigningLevel = /*#__PURE__*/function (CrossSigningLevel) {
+ CrossSigningLevel[CrossSigningLevel["MASTER"] = 4] = "MASTER";
+ CrossSigningLevel[CrossSigningLevel["USER_SIGNING"] = 2] = "USER_SIGNING";
+ CrossSigningLevel[CrossSigningLevel["SELF_SIGNING"] = 1] = "SELF_SIGNING";
+ return CrossSigningLevel;
+}({});
+/**
+ * Represents the ways in which we trust a user
+ */
+exports.CrossSigningLevel = CrossSigningLevel;
+class UserTrustLevel {
+ constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
+ this.crossSigningVerified = crossSigningVerified;
+ this.crossSigningVerifiedBefore = crossSigningVerifiedBefore;
+ this.tofu = tofu;
+ }
+
+ /**
+ * @returns true if this user is verified via any means
+ */
+ isVerified() {
+ return this.isCrossSigningVerified();
+ }
+
+ /**
+ * @returns true if this user is verified via cross signing
+ */
+ isCrossSigningVerified() {
+ return this.crossSigningVerified;
+ }
+
+ /**
+ * @returns true if we ever verified this user before (at least for
+ * the history of verifications observed by this device).
+ */
+ wasCrossSigningVerified() {
+ return this.crossSigningVerifiedBefore;
+ }
+
+ /**
+ * @returns true if this user's key is trusted on first use
+ */
+ isTofu() {
+ return this.tofu;
+ }
+}
+
+/**
+ * Represents the ways in which we trust a device.
+ *
+ * @deprecated Use {@link DeviceVerificationStatus}.
+ */
+exports.UserTrustLevel = UserTrustLevel;
+class DeviceTrustLevel extends _cryptoApi.DeviceVerificationStatus {
+ constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices, signedByOwner = false) {
+ super({
+ crossSigningVerified,
+ tofu,
+ localVerified,
+ trustCrossSignedDevices,
+ signedByOwner
+ });
+ }
+ static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
+ return new DeviceTrustLevel(userTrustLevel.isCrossSigningVerified(), userTrustLevel.isTofu(), localVerified, trustCrossSignedDevices, true);
+ }
+
+ /**
+ * @returns true if this device is verified via cross signing
+ */
+ isCrossSigningVerified() {
+ return this.crossSigningVerified;
+ }
+
+ /**
+ * @returns true if this device is verified locally
+ */
+ isLocallyVerified() {
+ return this.localVerified;
+ }
+
+ /**
+ * @returns true if this device is trusted from a user's key
+ * that is trusted on first use
+ */
+ isTofu() {
+ return this.tofu;
+ }
+}
+exports.DeviceTrustLevel = DeviceTrustLevel;
+function createCryptoStoreCacheCallbacks(store, olmDevice) {
+ return {
+ getCrossSigningKeyCache: async function (type, _expectedPublicKey) {
+ const key = await new Promise(resolve => {
+ store.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ store.getSecretStorePrivateKey(txn, resolve, type);
+ });
+ });
+ if (key && key.ciphertext) {
+ const pickleKey = Buffer.from(olmDevice.pickleKey);
+ const decrypted = await (0, _aes.decryptAES)(key, pickleKey, type);
+ return (0, _olmlib.decodeBase64)(decrypted);
+ } else {
+ return key;
+ }
+ },
+ storeCrossSigningKeyCache: async function (type, key) {
+ if (!(key instanceof Uint8Array)) {
+ throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
+ }
+ const pickleKey = Buffer.from(olmDevice.pickleKey);
+ const encryptedKey = await (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(key), pickleKey, type);
+ return store.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ store.storeSecretStorePrivateKey(txn, type, encryptedKey);
+ });
+ }
+ };
+}
+/**
+ * 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
+ */
+async function requestKeysDuringVerification(baseApis, userId, deviceId) {
+ // If this is a self-verification, ask the other party for keys
+ if (baseApis.getUserId() !== userId) {
+ return;
+ }
+ _logger.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((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 => {
+ _logger.logger.debug("Cross-signing: requesting secret", type, deviceId);
+ const {
+ promise
+ } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]);
+ const result = await promise;
+ const decoded = (0, _olmlib.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(resolve => {
+ setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout"));
+ });
+
+ // also request and cache the key backup key
+ const backupKeyPromise = (async () => {
+ const cachedKey = await client.crypto.getSessionBackupPrivateKey();
+ if (!cachedKey) {
+ _logger.logger.info("No cached backup key found. Requesting...");
+ const secretReq = client.requestSecret("m.megolm_backup.v1", [deviceId]);
+ const base64Key = await secretReq.promise;
+ _logger.logger.info("Got key backup key, decoding...");
+ const decodedKey = (0, _olmlib.decodeBase64)(base64Key);
+ _logger.logger.info("Decoded backup key, storing...");
+ await client.crypto.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey));
+ _logger.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.logger.info("Backup restored.");
+ });
+ }
+ })();
+
+ // We call getCrossSigningKey() for its side-effects
+ Promise.race([Promise.all([crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise]), timeout]).then(resolve, reject);
+ }).catch(e => {
+ _logger.logger.warn("Cross-signing: failure while requesting keys:", e);
+ });
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js
new file mode 100644
index 0000000000..31b8537428
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js
@@ -0,0 +1,860 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TrackingStatus = exports.DeviceList = void 0;
+var _logger = require("../logger");
+var _deviceinfo = require("./deviceinfo");
+var _CrossSigning = require("./CrossSigning");
+var olmlib = _interopRequireWildcard(require("./olmlib"));
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+var _utils = require("../utils");
+var _typedEventEmitter = require("../models/typed-event-emitter");
+var _index = require("./index");
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+/* 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
+let TrackingStatus = /*#__PURE__*/function (TrackingStatus) {
+ TrackingStatus[TrackingStatus["NotTracked"] = 0] = "NotTracked";
+ TrackingStatus[TrackingStatus["PendingDownload"] = 1] = "PendingDownload";
+ TrackingStatus[TrackingStatus["DownloadInProgress"] = 2] = "DownloadInProgress";
+ TrackingStatus[TrackingStatus["UpToDate"] = 3] = "UpToDate";
+ return TrackingStatus;
+}({}); // user-Id → device-Id → DeviceInfo
+exports.TrackingStatus = TrackingStatus;
+class DeviceList extends _typedEventEmitter.TypedEventEmitter {
+ constructor(baseApis, cryptoStore, olmDevice,
+ // Maximum number of user IDs per request to prevent server overload (#1619)
+ keyDownloadChunkSize = 250) {
+ super();
+ this.cryptoStore = cryptoStore;
+ this.keyDownloadChunkSize = keyDownloadChunkSize;
+ _defineProperty(this, "devices", {});
+ _defineProperty(this, "crossSigningInfo", {});
+ // map of identity keys to the user who owns it
+ _defineProperty(this, "userByIdentityKey", {});
+ // which users we are tracking device status for.
+ _defineProperty(this, "deviceTrackingStatus", {});
+ // 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.
+ _defineProperty(this, "syncToken", null);
+ _defineProperty(this, "keyDownloadsInProgressByUser", new Map());
+ // Set whenever changes are made other than setting the sync token
+ _defineProperty(this, "dirty", false);
+ // Promise resolved when device data is saved
+ _defineProperty(this, "savePromise", null);
+ // Function that resolves the save promise
+ _defineProperty(this, "resolveSavePromise", null);
+ // The time the save is scheduled for
+ _defineProperty(this, "savePromiseTime", null);
+ // The timer used to delay the save
+ _defineProperty(this, "saveTimer", null);
+ // True if we have fetched data from the server or loaded a non-empty
+ // set of device data from the store
+ _defineProperty(this, "hasFetched", null);
+ _defineProperty(this, "serialiser", void 0);
+ this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
+ }
+
+ /**
+ * Load the device tracking state from storage
+ */
+ async load() {
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => {
+ this.cryptoStore.getEndToEndDeviceData(txn, deviceData => {
+ this.hasFetched = Boolean(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;
+ }
+ }
+ }
+ stop() {
+ 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.
+ */
+ async saveIfDirty(delay = 500) {
+ 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.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.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.logger.error("Failed to save device tracking data", this.syncToken);
+ _logger.logger.error(err);
+ });
+ }, delay);
+ }
+ return savePromise;
+ }
+
+ /**
+ * Gets the sync token last set with setSyncToken
+ *
+ * @returns The sync token
+ */
+ getSyncToken() {
+ 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
+ */
+ setSyncToken(st) {
+ 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}.
+ */
+ downloadKeys(userIds, forceDownload) {
+ const usersToDownload = [];
+ const promises = [];
+ 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.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.logger.log("downloadKeys: downloading for", usersToDownload);
+ const downloadPromise = this.doKeyDownload(usersToDownload);
+ promises.push(downloadPromise);
+ }
+ if (promises.length === 0) {
+ _logger.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}.
+ */
+ getDevicesFromStore(userIds) {
+ const stored = 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
+ */
+ getKnownUserIds() {
+ 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.
+ */
+ getStoredDevicesForUser(userId) {
+ const devs = this.devices[userId];
+ if (!devs) {
+ return null;
+ }
+ const res = [];
+ for (const deviceId in devs) {
+ if (devs.hasOwnProperty(deviceId)) {
+ res.push(_deviceinfo.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.
+ */
+ getRawStoredDevicesForUser(userId) {
+ return this.devices[userId];
+ }
+ getStoredCrossSigningForUser(userId) {
+ if (!this.crossSigningInfo[userId]) return null;
+ return _CrossSigning.CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
+ }
+ storeCrossSigningForUser(userId, info) {
+ 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
+ */
+ getStoredDevice(userId, deviceId) {
+ const devs = this.devices[userId];
+ if (!devs?.[deviceId]) {
+ return undefined;
+ }
+ return _deviceinfo.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
+ */
+ getUserByIdentityKey(algorithm, senderKey) {
+ 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
+ */
+ getDeviceByIdentityKey(algorithm, senderKey) {
+ 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.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
+ */
+ storeDevicesForUser(userId, devices) {
+ 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.
+ *
+ */
+ startTrackingDeviceList(userId) {
+ // 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.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.
+ *
+ */
+ stopTrackingDeviceList(userId) {
+ if (this.deviceTrackingStatus[userId]) {
+ _logger.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.
+ */
+ stopTrackingAllDeviceLists() {
+ 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.
+ *
+ */
+ invalidateUserDeviceList(userId) {
+ if (this.deviceTrackingStatus[userId]) {
+ _logger.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).
+ */
+ refreshOutdatedDeviceLists() {
+ this.saveIfDirty();
+ const usersToDownload = [];
+ 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
+ */
+ setRawStoredDevicesForUser(userId, devices) {
+ // 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;
+ }
+ }
+ setRawStoredCrossSigningForUser(userId, info) {
+ 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.
+ */
+ doKeyDownload(users) {
+ if (users.length === 0) {
+ // nothing to do
+ return Promise.resolve();
+ }
+ const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => {
+ finished(true);
+ }, e => {
+ _logger.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 => {
+ this.emit(_index.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.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.logger.log("Device list for", u, "now up to date");
+ } else {
+ this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
+ }
+ }
+ });
+ this.saveIfDirty();
+ this.emit(_index.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).
+ */
+exports.DeviceList = DeviceList;
+class DeviceListUpdateSerialiser {
+ // 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
+ */
+ constructor(baseApis, olmDevice, deviceList) {
+ this.baseApis = baseApis;
+ this.olmDevice = olmDevice;
+ this.deviceList = deviceList;
+ _defineProperty(this, "downloadInProgress", false);
+ // users which are queued for download
+ // userId -> true
+ _defineProperty(this, "keyDownloadsQueuedByUser", {});
+ // deferred which is resolved when the queued users are downloaded.
+ // non-null indicates that we have users queued for download.
+ _defineProperty(this, "queuedQueryDeferred", void 0);
+ _defineProperty(this, "syncToken", void 0);
+ }
+
+ /**
+ * 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.
+ */
+ updateDevicesForUsers(users, syncToken) {
+ users.forEach(u => {
+ this.keyDownloadsQueuedByUser[u] = true;
+ });
+ if (!this.queuedQueryDeferred) {
+ this.queuedQueryDeferred = (0, _utils.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.logger.log("Queued key download for", users);
+ return this.queuedQueryDeferred.promise;
+ }
+
+ // start a new download.
+ return this.doQueuedQueries();
+ }
+ doQueuedQueries() {
+ 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.logger.log("Starting key download for", downloadUsers);
+ this.downloadInProgress = true;
+ const opts = {};
+ if (this.syncToken) {
+ opts.token = this.syncToken;
+ }
+ const factories = [];
+ 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));
+ }
+ (0, _utils.chunkPromises)(factories, 3).then(async responses => {
+ const dk = Object.assign({}, ...responses.map(res => res.device_keys || {}));
+ const masterKeys = Object.assign({}, ...responses.map(res => res.master_keys || {}));
+ const ssks = Object.assign({}, ...responses.map(res => res.self_signing_keys || {}));
+ const usks = 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 (0, _utils.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.logger.error(`Error processing keys for ${userId}:`, e);
+ }
+ }
+ }).then(() => {
+ _logger.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.logger.warn("Error downloading keys for " + downloadUsers + ":", e);
+ this.downloadInProgress = false;
+ deferred?.reject(e);
+ });
+ return deferred.promise;
+ }
+ async processQueryResponseForUser(userId, dkResponse, crossSigningResponse) {
+ _logger.logger.log("got device keys for " + userId + ":", dkResponse);
+ _logger.logger.log("got cross-signing keys for " + userId + ":", crossSigningResponse);
+ {
+ // map from deviceid -> deviceinfo for this user
+ const userStore = {};
+ const devs = this.deviceList.getRawStoredDevicesForUser(userId);
+ if (devs) {
+ Object.keys(devs).forEach(deviceId => {
+ const d = _deviceinfo.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 = {};
+ 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 _CrossSigning.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(_index.CryptoEvent.UserCrossSigningUpdated, userId);
+ }
+ }
+ }
+}
+async function updateStoredDeviceKeysForUser(olmDevice, userId, userStore, userResult, localUserId, localDeviceId) {
+ 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.logger.warn(`Local device ${deviceId} missing from sync, skipping removal`);
+ continue;
+ }
+ _logger.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.logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId);
+ continue;
+ }
+ if (deviceResult.device_id !== deviceId) {
+ _logger.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, userStore, deviceResult) {
+ 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.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.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.logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed");
+ return false;
+ }
+ } else {
+ userStore[deviceId] = deviceStore = new _deviceinfo.DeviceInfo(deviceId);
+ }
+ deviceStore.keys = deviceResult.keys || {};
+ deviceStore.algorithms = deviceResult.algorithms || [];
+ deviceStore.unsigned = unsigned;
+ deviceStore.signatures = signatures;
+ return true;
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js
new file mode 100644
index 0000000000..7bc39b0d92
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js
@@ -0,0 +1,342 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.EncryptionSetupOperation = exports.EncryptionSetupBuilder = void 0;
+var _logger = require("../logger");
+var _event = require("../models/event");
+var _CrossSigning = require("./CrossSigning");
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+var _httpApi = require("../http-api");
+var _client = require("../client");
+var _typedEventEmitter = require("../models/typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * 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.
+ */
+class EncryptionSetupBuilder {
+ /**
+ * @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
+ */
+ constructor(accountData, delegateCryptoCallbacks) {
+ _defineProperty(this, "accountDataClientAdapter", void 0);
+ _defineProperty(this, "crossSigningCallbacks", void 0);
+ _defineProperty(this, "ssssCryptoCallbacks", void 0);
+ _defineProperty(this, "crossSigningKeys", void 0);
+ _defineProperty(this, "keySignatures", void 0);
+ _defineProperty(this, "keyBackupInfo", void 0);
+ _defineProperty(this, "sessionBackupPrivateKey", void 0);
+ 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
+ */
+ addCrossSigningKeys(authUpload, keys) {
+ 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
+ */
+ addSessionBackup(keyBackupInfo) {
+ this.keyBackupInfo = keyBackupInfo;
+ }
+
+ /**
+ * Adds the session backup private key to be updated in the local cache
+ *
+ * Used after fixing the format of the key
+ *
+ */
+ addSessionBackupPrivateKeyToCache(privateKey) {
+ 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
+ *
+ */
+ addKeySignature(userId, deviceId, signature) {
+ if (!this.keySignatures) {
+ this.keySignatures = {};
+ }
+ const userSignatures = this.keySignatures[userId] || {};
+ this.keySignatures[userId] = userSignatures;
+ userSignatures[deviceId] = signature;
+ }
+ async setAccountData(type, content) {
+ await this.accountDataClientAdapter.setAccountData(type, content);
+ }
+
+ /**
+ * builds the operation containing all the parts that have been added to the builder
+ */
+ buildOperation() {
+ 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.
+ */
+ async persist(crypto) {
+ // store private keys in cache
+ if (this.crossSigningKeys) {
+ const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(crypto.cryptoStore, crypto.olmDevice);
+ for (const type of ["master", "self_signing", "user_signing"]) {
+ _logger.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.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.
+ */
+exports.EncryptionSetupBuilder = EncryptionSetupBuilder;
+class EncryptionSetupOperation {
+ /**
+ */
+ constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
+ this.accountData = accountData;
+ this.crossSigningKeys = crossSigningKeys;
+ this.keyBackupInfo = keyBackupInfo;
+ this.keySignatures = keySignatures;
+ }
+
+ /**
+ * Runs the (remaining part of, in the future) operation by sending requests to the server.
+ */
+ async apply(crypto) {
+ const baseApis = crypto.baseApis;
+ // upload cross-signing keys
+ if (this.crossSigningKeys) {
+ const keys = {};
+ for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
+ keys[name + "_key"] = 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);
+ });
+
+ // 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(_httpApi.Method.Put, "/room_keys/version/" + this.keyBackupInfo.version, undefined, {
+ algorithm: this.keyBackupInfo.algorithm,
+ auth_data: this.keyBackupInfo.auth_data
+ }, {
+ prefix: _httpApi.ClientPrefix.V3
+ });
+ } else {
+ // add new key backup
+ await baseApis.http.authedRequest(_httpApi.Method.Post, "/room_keys/version", undefined, this.keyBackupInfo, {
+ prefix: _httpApi.ClientPrefix.V3
+ });
+ }
+ }
+ }
+}
+
+/**
+ * Catches account data set by SecretStorage during bootstrapping by
+ * implementing the methods related to account data in MatrixClient
+ */
+exports.EncryptionSetupOperation = EncryptionSetupOperation;
+class AccountDataClientAdapter extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * @param existingValues - existing account data
+ */
+ constructor(existingValues) {
+ super();
+ this.existingValues = existingValues;
+ //
+ _defineProperty(this, "values", new Map());
+ }
+
+ /**
+ * @returns the content of the account data
+ */
+ getAccountDataFromServer(type) {
+ return Promise.resolve(this.getAccountData(type));
+ }
+
+ /**
+ * @returns the content of the account data
+ */
+ getAccountData(type) {
+ const modifiedValue = this.values.get(type);
+ if (modifiedValue) {
+ return modifiedValue;
+ }
+ const existingValue = this.existingValues.get(type);
+ if (existingValue) {
+ return existingValue.getContent();
+ }
+ return null;
+ }
+ setAccountData(type, content) {
+ 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 _event.MatrixEvent({
+ type,
+ content
+ });
+ this.emit(_client.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 {
+ constructor() {
+ _defineProperty(this, "privateKeys", new Map());
+ }
+ // cache callbacks
+ getCrossSigningKeyCache(type, expectedPublicKey) {
+ return this.getCrossSigningKey(type, expectedPublicKey);
+ }
+ storeCrossSigningKeyCache(type, key) {
+ this.privateKeys.set(type, key);
+ return Promise.resolve();
+ }
+
+ // non-cache callbacks
+ getCrossSigningKey(type, expectedPubkey) {
+ return Promise.resolve(this.privateKeys.get(type) ?? null);
+ }
+ saveCrossSigningKeys(privateKeys) {
+ 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 {
+ constructor(delegateCryptoCallbacks) {
+ this.delegateCryptoCallbacks = delegateCryptoCallbacks;
+ _defineProperty(this, "privateKeys", new Map());
+ }
+ async getSecretStorageKey({
+ keys
+ }, name) {
+ 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;
+ }
+ addPrivateKey(keyId, keyInfo, privKey) {
+ this.privateKeys.set(keyId, privKey);
+ // Also pass along to application to cache if it wishes
+ this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
+ }
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js
new file mode 100644
index 0000000000..1114de97d9
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js
@@ -0,0 +1,1162 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.WITHHELD_MESSAGES = exports.PayloadTooLargeError = exports.OlmDevice = void 0;
+var _logger = require("../logger");
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+var algorithms = _interopRequireWildcard(require("./algorithms"));
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+// 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;
+class PayloadTooLargeError extends Error {
+ constructor(...args) {
+ super(...args);
+ _defineProperty(this, "data", {
+ errcode: "M_TOO_LARGE",
+ error: "Payload too large for encrypted message"
+ });
+ }
+}
+exports.PayloadTooLargeError = PayloadTooLargeError;
+function checkPayloadLength(payloadString) {
+ 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.`);
+ }
+}
+
+/** data stored in the session store about an inbound group session */
+
+/* eslint-disable camelcase */
+
+/* eslint-enable camelcase */
+
+/**
+ * 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.
+ */
+class OlmDevice {
+ // set by consumers
+
+ constructor(cryptoStore) {
+ this.cryptoStore = cryptoStore;
+ _defineProperty(this, "pickleKey", "DEFAULT_KEY");
+ // set by consumers
+ /** Curve25519 key for the account, unknown until we load the account from storage in init() */
+ _defineProperty(this, "deviceCurve25519Key", null);
+ /** Ed25519 key for the account, unknown until we load the account from storage in init() */
+ _defineProperty(this, "deviceEd25519Key", null);
+ _defineProperty(this, "maxOneTimeKeys", null);
+ // we don't bother stashing outboundgroupsessions in the cryptoStore -
+ // instead we keep them here.
+ _defineProperty(this, "outboundGroupSessionStore", {});
+ // 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>}"
+ _defineProperty(this, "inboundGroupSessionMessageIndexes", {});
+ // Keep track of sessions that we're starting, so that we don't start
+ // multiple sessions for the same device at the same time.
+ _defineProperty(this, "sessionsInProgress", {});
+ // set by consumers
+ // Used by olm to serialise prekey message decryptions
+ _defineProperty(this, "olmPrekeyPromise", Promise.resolve());
+ }
+
+ /**
+ * @returns The version of Olm.
+ */
+ static getOlmVersion() {
+ 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
+ */
+ async init({
+ pickleKey,
+ fromExportedDevice
+ } = {}) {
+ let e2eKeys;
+ const account = new global.Olm.Account();
+ try {
+ if (fromExportedDevice) {
+ if (pickleKey) {
+ _logger.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
+ */
+ async initialiseFromExportedDevice(exportedData, account) {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.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);
+ }
+ async initialiseAccount(account) {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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
+ */
+ getAccount(txn, func) {
+ this.cryptoStore.getAccount(txn, pickledAccount => {
+ 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
+ */
+ storeAccount(txn, account) {
+ 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
+ */
+ async export() {
+ const result = {
+ pickleKey: this.pickleKey
+ };
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.cryptoStore.getAccount(txn, pickledAccount => {
+ 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;
+ }
+
+ /**
+ * 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
+ */
+ getSession(deviceKey, sessionId, txn, func) {
+ this.cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, sessionInfo => {
+ 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
+ */
+ unpickleSession(sessionInfo, func) {
+ const session = new global.Olm.Session();
+ try {
+ session.unpickle(this.pickleKey, sessionInfo.session);
+ const unpickledSessInfo = 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
+ */
+ saveSession(deviceKey, sessionInfo, txn) {
+ const sessionId = sessionInfo.session.session_id();
+ _logger.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
+ */
+ getUtility(func) {
+ 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
+ */
+ async sign(message) {
+ let result;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.getAccount(txn, 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.
+ */
+ async getOneTimeKeys() {
+ let result;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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
+ */
+ maxNumberOfOneTimeKeys() {
+ return this.maxOneTimeKeys ?? -1;
+ }
+
+ /**
+ * Marks all of the one-time keys as published.
+ */
+ async markKeysAsPublished() {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.getAccount(txn, 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
+ */
+ generateOneTimeKeys(numKeys) {
+ return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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
+ */
+ async generateFallbackKey() {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.getAccount(txn, account => {
+ account.generate_fallback_key();
+ this.storeAccount(txn, account);
+ });
+ });
+ }
+ async getFallbackKey() {
+ let result;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.getAccount(txn, account => {
+ result = JSON.parse(account.unpublished_fallback_key());
+ });
+ });
+ return result;
+ }
+ async forgetOldFallbackKey() {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.getAccount(txn, 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.
+ */
+ async createOutboundSession(theirIdentityKey, theirOneTimeKey) {
+ let newSessionId;
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.getAccount(txn, account => {
+ const session = new global.Olm.Session();
+ try {
+ session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
+ newSessionId = session.session_id();
+ this.storeAccount(txn, account);
+ const sessionInfo = {
+ 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.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).
+ */
+ async createInboundSession(theirDeviceIdentityKey, messageType, ciphertext) {
+ if (messageType !== 0) {
+ throw new Error("Need messageType == 0 to create inbound session");
+ }
+ let result; // eslint-disable-line camelcase
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.getAccount(txn, 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 = {
+ 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.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
+ */
+ async getSessionIdsForDevice(theirDeviceIdentityKey) {
+ const log = _logger.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;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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
+ */
+ async getSessionIdForDevice(theirDeviceIdentityKey, nowait = false, log) {
+ 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
+ */
+ async getSessionInfoForDevice(deviceIdentityKey, nowait = false, log = _logger.logger) {
+ 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 = [];
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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 => {
+ 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
+ */
+ async encryptMessage(theirDeviceIdentityKey, sessionId, payloadString) {
+ checkPayloadLength(payloadString);
+ let res;
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => {
+ const sessionDesc = sessionInfo.session.describe();
+ _logger.logger.log("encryptMessage: Olm Session ID " + sessionId + " to " + theirDeviceIdentityKey + ": " + sessionDesc);
+ res = sessionInfo.session.encrypt(payloadString);
+ this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
+ });
+ }, _logger.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.
+ */
+ async decryptMessage(theirDeviceIdentityKey, sessionId, messageType, ciphertext) {
+ let payloadString;
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => {
+ const sessionDesc = sessionInfo.session.describe();
+ _logger.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.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.
+ */
+ async matchesSession(theirDeviceIdentityKey, sessionId, messageType, ciphertext) {
+ if (messageType !== 0) {
+ return false;
+ }
+ let matches;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => {
+ this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => {
+ matches = sessionInfo.session.matches_inbound(ciphertext);
+ });
+ }, _logger.logger.withPrefix("[matchesSession]"));
+ return matches;
+ }
+ async recordSessionProblem(deviceKey, type, fixed) {
+ _logger.logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`);
+ await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
+ }
+ sessionMayHaveProblems(deviceKey, timestamp) {
+ return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
+ }
+ filterOutNotifiedErrorDevices(devices) {
+ return this.cryptoStore.filterOutNotifiedErrorDevices(devices);
+ }
+
+ // Outbound group session
+ // ======================
+
+ /**
+ * store an OutboundGroupSession in outboundGroupSessionStore
+ *
+ * @internal
+ */
+ saveOutboundGroupSession(session) {
+ this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey);
+ }
+
+ /**
+ * extract an OutboundGroupSession from outboundGroupSessionStore and call the
+ * given function
+ *
+ * @returns result of func
+ * @internal
+ */
+ getOutboundGroupSession(sessionId, func) {
+ 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.
+ */
+ createOutboundGroupSession() {
+ 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
+ */
+ encryptGroupMessage(sessionId, payloadString) {
+ _logger.logger.log(`encrypting msg with megolm session ${sessionId}`);
+ checkPayloadLength(payloadString);
+ return this.getOutboundGroupSession(sessionId, session => {
+ 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.
+ */
+ getOutboundGroupSessionKey(sessionId) {
+ return this.getOutboundGroupSession(sessionId, function (session) {
+ 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
+ */
+ unpickleInboundGroupSession(sessionData, func) {
+ 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
+ */
+ getInboundGroupSession(roomId, senderKey, sessionId, txn, func) {
+ this.cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, (sessionData, withheld) => {
+ 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 => {
+ 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
+ */
+ async addInboundGroupSession(roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat, extraSessionData = {}) {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => {
+ /* if we already have this session, consider updating it */
+ this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (existingSession, existingSessionData) => {
+ // 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.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.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.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.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.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.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`
+ */
+ async addInboundGroupSessionWithheld(roomId, senderKey, sessionId, code, reason) {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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
+ */
+ async decryptGroupMessage(roomId, senderKey, sessionId, body, eventId, timestamp) {
+ let result = 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;
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.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;
+ try {
+ res = session.decrypt(body);
+ } catch (e) {
+ if (e?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
+ error = new algorithms.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", calculateWithheldMessage(withheld), {
+ session: senderKey + "|" + sessionId
+ });
+ } else {
+ error = e;
+ }
+ return;
+ }
+ let plaintext = res.plaintext;
+ if (plaintext === undefined) {
+ // @ts-ignore - Compatibility for older olm versions.
+ plaintext = res;
+ } 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.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
+ */
+ async hasInboundSessionKeys(roomId, senderKey, sessionId) {
+ let result;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.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.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.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).
+ */
+ async getInboundGroupSessionKey(roomId, senderKey, sessionId, chainIndex) {
+ let result = null;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.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.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
+ */
+ exportInboundGroupSession(senderKey, sessionId, sessionData) {
+ 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
+ };
+ });
+ }
+ async getSharedHistoryInboundGroupSessions(roomId) {
+ let result;
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => {
+ result = this.cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
+ }, _logger.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".
+ */
+ verifySignature(key, message, signature) {
+ this.getUtility(function (util) {
+ util.ed25519_verify(key, message, signature);
+ });
+ }
+}
+exports.OlmDevice = OlmDevice;
+const WITHHELD_MESSAGES = {
+ "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
+ */
+exports.WITHHELD_MESSAGES = WITHHELD_MESSAGES;
+function calculateWithheldMessage(withheld) {
+ 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";
+ }
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js
new file mode 100644
index 0000000000..a9d056c5ea
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js
@@ -0,0 +1,406 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomKeyRequestState = exports.OutgoingRoomKeyRequestManager = void 0;
+var _uuid = require("uuid");
+var _logger = require("../logger");
+var _event = require("../@types/event");
+var _utils = require("../utils");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * 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) <---------------------------+
+ * ```
+ */
+let RoomKeyRequestState = /*#__PURE__*/function (RoomKeyRequestState) {
+ RoomKeyRequestState[RoomKeyRequestState["Unsent"] = 0] = "Unsent";
+ RoomKeyRequestState[RoomKeyRequestState["Sent"] = 1] = "Sent";
+ RoomKeyRequestState[RoomKeyRequestState["CancellationPending"] = 2] = "CancellationPending";
+ RoomKeyRequestState[RoomKeyRequestState["CancellationPendingAndWillResend"] = 3] = "CancellationPendingAndWillResend";
+ return RoomKeyRequestState;
+}({});
+exports.RoomKeyRequestState = RoomKeyRequestState;
+class OutgoingRoomKeyRequestManager {
+ constructor(baseApis, deviceId, cryptoStore) {
+ this.baseApis = baseApis;
+ this.deviceId = deviceId;
+ this.cryptoStore = cryptoStore;
+ // handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
+ // if the callback has been set, or if it is still running.
+ _defineProperty(this, "sendOutgoingRoomKeyRequestsTimer", void 0);
+ // sanity check to ensure that we don't end up with two concurrent runs
+ // of sendOutgoingRoomKeyRequests
+ _defineProperty(this, "sendOutgoingRoomKeyRequestsRunning", false);
+ _defineProperty(this, "clientRunning", true);
+ }
+
+ /**
+ * Called when the client is stopped. Stops any running background processes.
+ */
+ stop() {
+ _logger.logger.log("stopping OutgoingRoomKeyRequestManager");
+ // stop the timer on the next run
+ this.clientRunning = false;
+ }
+
+ /**
+ * Send any requests that have been queued
+ */
+ sendQueuedRequests() {
+ 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)
+ */
+ async queueRoomKeyRequest(requestBody, recipients, resend = false) {
+ 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.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.
+ */
+ cancelRoomKeyRequest(requestBody) {
+ return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(req => {
+ 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.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.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.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}
+ */
+ getOutgoingSentRoomKeyRequest(userId, deviceId) {
+ 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.
+ */
+ async cancelAndResendAllOutgoingRequests() {
+ 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
+ startTimer() {
+ if (this.sendOutgoingRoomKeyRequestsTimer) {
+ return;
+ }
+ const startSendingOutgoingRoomKeyRequests = () => {
+ 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.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).
+ async sendOutgoingRoomKeyRequests() {
+ 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.logger.error("Error sending room key request; will retry later.", e);
+ this.sendOutgoingRoomKeyRequestsTimer = undefined;
+ }
+ }
+
+ // given a RoomKeyRequest, send it and update the request record
+ sendOutgoingRoomKeyRequest(req) {
+ _logger.logger.log(`Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + `(id ${req.requestId})`);
+ const 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.
+ sendOutgoingRoomKeyRequestCancellation(req, andResend = false) {
+ _logger.logger.log(`Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + `(cancellation id ${req.cancellationTxnId})`);
+ const 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
+ sendMessageToDevices(message, recipients, txnId) {
+ const contentMap = new _utils.MapWithDefault(() => new Map());
+ for (const recip of recipients) {
+ const userDeviceMap = contentMap.getOrCreate(recip.userId);
+ userDeviceMap.set(recip.deviceId, _objectSpread(_objectSpread({}, message), {}, {
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ }));
+ }
+ return this.baseApis.sendToDevice(_event.EventType.RoomKeyRequest, contentMap, txnId);
+ }
+}
+exports.OutgoingRoomKeyRequestManager = OutgoingRoomKeyRequestManager;
+function stringifyRequestBody(requestBody) {
+ // 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) {
+ return `[${recipients.map(r => `${r.userId}:${r.deviceId}`).join(",")}]`;
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js
new file mode 100644
index 0000000000..24dd53ed3c
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js
@@ -0,0 +1,60 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomList = void 0;
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+/* eslint-disable camelcase */
+
+/* eslint-enable camelcase */
+
+class RoomList {
+ constructor(cryptoStore) {
+ this.cryptoStore = cryptoStore;
+ // Object of roomId -> room e2e info object (body of the m.room.encryption event)
+ _defineProperty(this, "roomEncryption", {});
+ }
+ async init() {
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => {
+ this.cryptoStore.getEndToEndRooms(txn, result => {
+ this.roomEncryption = result;
+ });
+ });
+ }
+ getRoomEncryption(roomId) {
+ return this.roomEncryption[roomId] || null;
+ }
+ isRoomEncrypted(roomId) {
+ return Boolean(this.getRoomEncryption(roomId));
+ }
+ async setRoomEncryption(roomId, roomInfo) {
+ // 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.IndexedDBCryptoStore.STORE_ROOMS], txn => {
+ this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
+ });
+ }
+}
+exports.RoomList = RoomList; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretSharing.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretSharing.js
new file mode 100644
index 0000000000..805fd64471
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretSharing.js
@@ -0,0 +1,199 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SecretSharing = void 0;
+var _uuid = require("uuid");
+var _utils = require("../utils");
+var _event = require("../@types/event");
+var _logger = require("../logger");
+var olmlib = _interopRequireWildcard(require("./olmlib"));
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2019-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.
+ */
+class SecretSharing {
+ constructor(baseApis, cryptoCallbacks) {
+ this.baseApis = baseApis;
+ this.cryptoCallbacks = cryptoCallbacks;
+ _defineProperty(this, "requests", new Map());
+ }
+
+ /**
+ * Request a secret from another device
+ *
+ * @param name - the name of the secret to request
+ * @param devices - the devices to request the secret from
+ */
+ request(name, devices) {
+ const requestId = this.baseApis.makeTxnId();
+ const deferred = (0, _utils.defer)();
+ this.requests.set(requestId, {
+ name,
+ devices,
+ deferred
+ });
+ const cancel = reason => {
+ // send cancellation event
+ const cancelData = {
+ action: "request_cancellation",
+ requesting_device_id: this.baseApis.deviceId,
+ request_id: requestId
+ };
+ const toDevice = 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,
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ const toDevice = new Map();
+ for (const device of devices) {
+ toDevice.set(device, requestData);
+ }
+ _logger.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
+ };
+ }
+ async onRequestReceived(event) {
+ 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.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.logger.info(`Preparing ${content.name} secret for ${deviceId}`);
+ const payload = {
+ type: "m.secret.send",
+ content: {
+ request_id: content.request_id,
+ secret: secret
+ }
+ };
+ const encryptedContent = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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.logger.info(`Sending ${content.name} secret for ${deviceId}`);
+ this.baseApis.sendToDevice("m.room.encrypted", contentMap);
+ } else {
+ _logger.logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
+ }
+ }
+ }
+ onSecretReceived(event) {
+ 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.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.logger.error("sending device does not belong to the user it claims to be from");
+ return;
+ }
+ _logger.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.logger.log("secret share from unknown device with key", event.getSenderKey());
+ return;
+ }
+ if (!requestControl.devices.includes(deviceInfo.deviceId)) {
+ _logger.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.logger.log("secret share from unverified device");
+ return;
+ }
+ _logger.logger.log(`Successfully received secret ${requestControl.name} ` + `from ${deviceInfo.deviceId}`);
+ requestControl.deferred.resolve(content.secret);
+ }
+ }
+}
+exports.SecretSharing = SecretSharing; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js
new file mode 100644
index 0000000000..9b363f359c
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js
@@ -0,0 +1,119 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SecretStorage = void 0;
+var _secretStorage = require("../secret-storage");
+var _SecretSharing = require("./SecretSharing");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/* re-exports for backwards compatibility */
+
+/**
+ * Implements Secure Secret Storage and Sharing (MSC1946)
+ *
+ * @deprecated This is just a backwards-compatibility hack which will be removed soon.
+ * Use {@link SecretStorage.ServerSideSecretStorageImpl} from `../secret-storage` and/or {@link SecretSharing} from `./SecretSharing`.
+ */
+class SecretStorage {
+ // In its 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, we added
+ // an extra, optional param for a real matrix client, so you can not pass it as long
+ // as you don't request any secrets.
+ //
+ // Nowadays, the whole class is scheduled for destruction, once we get rid of the legacy
+ // Crypto impl that exposes it.
+ constructor(accountDataAdapter, cryptoCallbacks, baseApis) {
+ _defineProperty(this, "storageImpl", void 0);
+ _defineProperty(this, "sharingImpl", void 0);
+ this.storageImpl = new _secretStorage.ServerSideSecretStorageImpl(accountDataAdapter, cryptoCallbacks);
+ this.sharingImpl = new _SecretSharing.SecretSharing(baseApis, cryptoCallbacks);
+ }
+ getDefaultKeyId() {
+ return this.storageImpl.getDefaultKeyId();
+ }
+ setDefaultKeyId(keyId) {
+ return this.storageImpl.setDefaultKeyId(keyId);
+ }
+
+ /**
+ * Add a key for encrypting secrets.
+ */
+ addKey(algorithm, opts = {}, keyId) {
+ return this.storageImpl.addKey(algorithm, opts, keyId);
+ }
+
+ /**
+ * Get the key information for a given ID.
+ */
+ getKey(keyId) {
+ return this.storageImpl.getKey(keyId);
+ }
+
+ /**
+ * Check whether we have a key with a given ID.
+ */
+ hasKey(keyId) {
+ return this.storageImpl.hasKey(keyId);
+ }
+
+ /**
+ * Check whether a key matches what we expect based on the key info
+ */
+ checkKey(key, info) {
+ return this.storageImpl.checkKey(key, info);
+ }
+
+ /**
+ * Store an encrypted secret on the server
+ */
+ store(name, secret, keys) {
+ return this.storageImpl.store(name, secret, keys);
+ }
+
+ /**
+ * Get a secret from storage.
+ */
+ get(name) {
+ return this.storageImpl.get(name);
+ }
+
+ /**
+ * Check if a secret is stored on the server.
+ */
+ async isStored(name) {
+ return this.storageImpl.isStored(name);
+ }
+
+ /**
+ * Request a secret from another device
+ */
+ request(name, devices) {
+ return this.sharingImpl.request(name, devices);
+ }
+ onRequestReceived(event) {
+ return this.sharingImpl.onRequestReceived(event);
+ }
+ onSecretReceived(event) {
+ this.sharingImpl.onSecretReceived(event);
+ }
+}
+exports.SecretStorage = SecretStorage; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js
new file mode 100644
index 0000000000..e48c59446c
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js
@@ -0,0 +1,127 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.calculateKeyCheck = calculateKeyCheck;
+exports.decryptAES = decryptAES;
+exports.encryptAES = encryptAES;
+var _olmlib = require("./olmlib");
+var _crypto = require("./crypto");
+/*
+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.
+*/
+
+// salt for HKDF, with 8 bytes of zeros
+const zeroSalt = new Uint8Array(8);
+/**
+ * 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
+ */
+async function encryptAES(data, key, name, ivStr) {
+ let iv;
+ if (ivStr) {
+ iv = (0, _olmlib.decodeBase64)(ivStr);
+ } else {
+ iv = new Uint8Array(16);
+ _crypto.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 _crypto.TextEncoder().encode(data);
+ const ciphertext = await _crypto.subtleCrypto.encrypt({
+ name: "AES-CTR",
+ counter: iv,
+ length: 64
+ }, aesKey, encodedData);
+ const hmac = await _crypto.subtleCrypto.sign({
+ name: "HMAC"
+ }, hmacKey, ciphertext);
+ return {
+ iv: (0, _olmlib.encodeBase64)(iv),
+ ciphertext: (0, _olmlib.encodeBase64)(ciphertext),
+ mac: (0, _olmlib.encodeBase64)(hmac)
+ };
+}
+
+/**
+ * decrypt a string
+ *
+ * @param data - the encrypted data
+ * @param key - the encryption key to use
+ * @param name - the name of the secret
+ */
+async function decryptAES(data, key, name) {
+ const [aesKey, hmacKey] = await deriveKeys(key, name);
+ const ciphertext = (0, _olmlib.decodeBase64)(data.ciphertext);
+ if (!(await _crypto.subtleCrypto.verify({
+ name: "HMAC"
+ }, hmacKey, (0, _olmlib.decodeBase64)(data.mac), ciphertext))) {
+ throw new Error(`Error decrypting secret ${name}: bad MAC`);
+ }
+ const plaintext = await _crypto.subtleCrypto.decrypt({
+ name: "AES-CTR",
+ counter: (0, _olmlib.decodeBase64)(data.iv),
+ length: 64
+ }, aesKey, ciphertext);
+ return new TextDecoder().decode(new Uint8Array(plaintext));
+}
+async function deriveKeys(key, name) {
+ const hkdfkey = await _crypto.subtleCrypto.importKey("raw", key, {
+ name: "HKDF"
+ }, false, ["deriveBits"]);
+ const keybits = await _crypto.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 _crypto.TextEncoder().encode(name),
+ hash: "SHA-256"
+ }, hkdfkey, 512);
+ const aesKey = keybits.slice(0, 32);
+ const hmacKey = keybits.slice(32);
+ const aesProm = _crypto.subtleCrypto.importKey("raw", aesKey, {
+ name: "AES-CTR"
+ }, false, ["encrypt", "decrypt"]);
+ const hmacProm = _crypto.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.
+ */
+function calculateKeyCheck(key, iv) {
+ return encryptAES(ZERO_STR, key, "", iv);
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js
new file mode 100644
index 0000000000..803b5cf8fd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js
@@ -0,0 +1,226 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.UnknownDeviceError = exports.EncryptionAlgorithm = exports.ENCRYPTION_CLASSES = exports.DecryptionError = exports.DecryptionAlgorithm = exports.DECRYPTION_CLASSES = void 0;
+exports.registerAlgorithm = registerAlgorithm;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+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
+ */
+
+/**
+ * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
+ */
+const ENCRYPTION_CLASSES = new Map();
+exports.ENCRYPTION_CLASSES = ENCRYPTION_CLASSES;
+/**
+ * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class
+ */
+const DECRYPTION_CLASSES = new Map();
+exports.DECRYPTION_CLASSES = DECRYPTION_CLASSES;
+/**
+ * base type for encryption implementations
+ */
+class EncryptionAlgorithm {
+ /**
+ * @param params - parameters
+ */
+ constructor(params) {
+ _defineProperty(this, "userId", void 0);
+ _defineProperty(this, "deviceId", void 0);
+ _defineProperty(this, "crypto", void 0);
+ _defineProperty(this, "olmDevice", void 0);
+ _defineProperty(this, "baseApis", void 0);
+ _defineProperty(this, "roomId", void 0);
+ 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
+ */
+ prepareToEncrypt(room) {}
+
+ /**
+ * Encrypt a message event
+ *
+ * @public
+ *
+ * @param content - event content
+ *
+ * @returns Promise which resolves to the new event body
+ */
+
+ /**
+ * 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
+ */
+ onRoomMembership(event, member, oldMembership) {}
+}
+
+/**
+ * base type for decryption implementations
+ */
+exports.EncryptionAlgorithm = EncryptionAlgorithm;
+class DecryptionAlgorithm {
+ constructor(params) {
+ _defineProperty(this, "userId", void 0);
+ _defineProperty(this, "crypto", void 0);
+ _defineProperty(this, "olmDevice", void 0);
+ _defineProperty(this, "baseApis", void 0);
+ _defineProperty(this, "roomId", void 0);
+ 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.
+ */
+
+ /**
+ * Handle a key event
+ *
+ * @param params - event key event
+ */
+ async onRoomKeyEvent(params) {
+ // ignore by default
+ }
+
+ /**
+ * Import a room key
+ *
+ * @param opts - object
+ */
+ async importRoomKey(session, opts) {
+ // 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.
+ */
+ hasKeysForKeyRequest(keyRequest) {
+ return Promise.resolve(false);
+ }
+
+ /**
+ * Send the response to a room key request
+ *
+ */
+ shareKeysWithDevice(keyRequest) {
+ 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
+ */
+ async retryDecryptionFromSender(senderKey) {
+ // ignore by default
+ return false;
+ }
+}
+
+/**
+ * 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.
+ */
+exports.DecryptionAlgorithm = DecryptionAlgorithm;
+class DecryptionError extends Error {
+ constructor(code, msg, details) {
+ super(msg);
+ this.code = code;
+ _defineProperty(this, "detailedString", void 0);
+ this.code = code;
+ this.name = "DecryptionError";
+ this.detailedString = detailedStringForDecryptionError(this, details);
+ }
+}
+exports.DecryptionError = DecryptionError;
+function detailedStringForDecryptionError(err, details) {
+ let result = err.name + "[msg: " + err.message;
+ if (details) {
+ result += ", " + Object.keys(details).map(k => k + ": " + details[k]).join(", ");
+ }
+ result += "]";
+ return result;
+}
+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
+ */
+ constructor(msg, devices, event) {
+ super(msg);
+ this.devices = devices;
+ this.event = event;
+ 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
+ */
+exports.UnknownDeviceError = UnknownDeviceError;
+function registerAlgorithm(algorithm, encryptor, decryptor) {
+ ENCRYPTION_CLASSES.set(algorithm, encryptor);
+ DECRYPTION_CLASSES.set(algorithm, decryptor);
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js
new file mode 100644
index 0000000000..c49d64cef4
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js
@@ -0,0 +1,18 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+require("./olm");
+require("./megolm");
+var _base = require("./base");
+Object.keys(_base).forEach(function (key) {
+ if (key === "default" || key === "__esModule") return;
+ if (key in exports && exports[key] === _base[key]) return;
+ Object.defineProperty(exports, key, {
+ enumerable: true,
+ get: function () {
+ return _base[key];
+ }
+ });
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js
new file mode 100644
index 0000000000..a1f5c4fe72
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js
@@ -0,0 +1,1682 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MegolmEncryption = exports.MegolmDecryption = void 0;
+exports.isRoomSharedHistory = isRoomSharedHistory;
+var _uuid = require("uuid");
+var _logger = require("../../logger");
+var olmlib = _interopRequireWildcard(require("../olmlib"));
+var _base = require("./base");
+var _OlmDevice = require("../OlmDevice");
+var _event = require("../../@types/event");
+var _OutgoingRoomKeyRequestManager = require("../OutgoingRoomKeyRequestManager");
+var _utils = require("../../utils");
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+// determine whether the key can be shared with invitees
+function isRoomSharedHistory(room) {
+ 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);
+}
+
+// map user Id → device Id → IBlockedDevice
+
+/**
+ * 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 => {
+ 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 */
+
+/**
+ * @internal
+ */
+class OutboundSessionInfo {
+ /**
+ * @param sharedHistory - whether the session can be freely shared with
+ * other group members, according to the room history visibility settings
+ */
+ constructor(sessionId, sharedHistory = false) {
+ this.sessionId = sessionId;
+ this.sharedHistory = sharedHistory;
+ /** number of times this session has been used */
+ _defineProperty(this, "useCount", 0);
+ /** when the session was created (ms since the epoch) */
+ _defineProperty(this, "creationTime", void 0);
+ /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */
+ _defineProperty(this, "sharedWithDevices", new _utils.MapWithDefault(() => new Map()));
+ _defineProperty(this, "blockedDevicesNotified", new _utils.MapWithDefault(() => new Map()));
+ this.creationTime = new Date().getTime();
+ }
+
+ /**
+ * Check if it's time to rotate the session
+ */
+ needsRotation(rotationPeriodMsgs, rotationPeriodMs) {
+ const sessionLifetime = new Date().getTime() - this.creationTime;
+ if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
+ _logger.logger.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms");
+ return true;
+ }
+ return false;
+ }
+ markSharedWithDevice(userId, deviceId, deviceKey, chainIndex) {
+ this.sharedWithDevices.getOrCreate(userId).set(deviceId, {
+ deviceKey,
+ messageIndex: chainIndex
+ });
+ }
+ markNotifiedBlockedDevice(userId, deviceId) {
+ 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.
+ */
+ sharedWithTooManyDevices(devicesInRoom) {
+ for (const [userId, devices] of this.sharedWithDevices) {
+ if (!devicesInRoom.has(userId)) {
+ _logger.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.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}
+ */
+class MegolmEncryption extends _base.EncryptionAlgorithm {
+ constructor(params) {
+ super(params);
+ // 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).
+ _defineProperty(this, "setupPromise", Promise.resolve(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).
+ _defineProperty(this, "outboundSessions", {});
+ _defineProperty(this, "sessionRotationPeriodMsgs", void 0);
+ _defineProperty(this, "sessionRotationPeriodMs", void 0);
+ _defineProperty(this, "encryptionPreparation", void 0);
+ _defineProperty(this, "roomId", void 0);
+ _defineProperty(this, "prefixedLogger", void 0);
+ this.roomId = params.roomId;
+ this.prefixedLogger = _logger.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.
+ */
+ async ensureOutboundSession(room, devicesInRoom, blocked, singleOlmCreationPhase = false) {
+ // 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 => {
+ 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;
+ }
+ async prepareSession(devicesInRoom, sharedHistory, session) {
+ // 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;
+ }
+ async shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session) {
+ // now check if we need to share with any devices
+ const shareMap = {};
+ 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 = {
+ 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 () => {
+ // 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 () => {
+ 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 = [];
+
+ // 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 = [];
+ 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 () => {
+ // 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 = new _utils.MapWithDefault(() => []);
+ const failedServerMap = new Set();
+ for (const server of failedServers) {
+ failedServerMap.add(server);
+ }
+ const failedDevices = [];
+ 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 () => {
+ 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 = new _utils.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
+ */
+ async prepareNewSession(sharedHistory) {
+ 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.
+ */
+ getDevicesWithoutSessions(deviceMap, devicesByUser, noOlmDevices = []) {
+ 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
+ */
+ splitDevices(devicesByUser) {
+ const maxDevicesPerRequest = 20;
+
+ // use an array where the slices of a content map gets stored
+ let currentSlice = [];
+ 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.
+ */
+ encryptAndSendKeysToDevices(session, chainIndex, devices, payload) {
+ 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.
+ */
+ async sendBlockedNotificationsToDevices(session, userDeviceMap, payload) {
+ const contentMap = new _utils.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 = _objectSpread(_objectSpread({}, payload), {}, {
+ code: blockedInfo.code,
+ reason: blockedInfo.reason,
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ });
+ 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
+ */
+ async reshareKeyWithDevice(senderKey, sessionId, userId, device) {
+ 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 = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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.
+ */
+ async shareKeyWithDevices(session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers) {
+ 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);
+ }
+ async shareKeyWithOlmSessions(session, key, payload, deviceMap) {
+ 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
+ */
+ async notifyFailedOlmDevices(session, key, failedDevices) {
+ 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 = new _utils.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: _OlmDevice.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
+ */
+ async notifyBlockedDevices(session, devicesByUser) {
+ const payload = {
+ 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
+ */
+ prepareToEncrypt(room) {
+ 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 = () => cancelled;
+ this.encryptionPreparation = {
+ startTime: Date.now(),
+ promise: (async () => {
+ 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: () => {
+ // 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
+ */
+ async encryptMessage(room, eventType, content) {
+ 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 = {
+ 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;
+ }
+ isVerificationEvent(eventType, content) {
+ switch (eventType) {
+ case _event.EventType.KeyVerificationCancel:
+ case _event.EventType.KeyVerificationDone:
+ case _event.EventType.KeyVerificationMac:
+ case _event.EventType.KeyVerificationStart:
+ case _event.EventType.KeyVerificationKey:
+ case _event.EventType.KeyVerificationReady:
+ case _event.EventType.KeyVerificationAccept:
+ {
+ return true;
+ }
+ case _event.EventType.RoomMessage:
+ {
+ return content["msgtype"] === _event.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.
+ */
+ forceDiscardSession() {
+ 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.
+ */
+ checkForUnknownDevices(devicesInRoom) {
+ const unknownDevices = new _utils.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 _base.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.
+ */
+ removeUnknownDevices(devicesInRoom) {
+ 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`.
+ */
+
+ async getDevicesInRoom(room, forceDistributeToUnverified = false, isCancelled) {
+ 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 _utils.MapWithDefault(() => 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 (0, _utils.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: _OlmDevice.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}
+ */
+exports.MegolmEncryption = MegolmEncryption;
+class MegolmDecryption extends _base.DecryptionAlgorithm {
+ constructor(params) {
+ super(params);
+ // 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
+ _defineProperty(this, "pendingEvents", new Map());
+ // this gets stubbed out by the unit tests.
+ _defineProperty(this, "olmlib", olmlib);
+ _defineProperty(this, "roomId", void 0);
+ _defineProperty(this, "prefixedLogger", void 0);
+ this.roomId = params.roomId;
+ this.prefixedLogger = _logger.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.
+ */
+ async decryptEvent(event) {
+ const content = event.getWireContent();
+ if (!content.sender_key || !content.session_id || !content.ciphertext) {
+ throw new _base.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;
+ try {
+ res = await this.olmDevice.decryptGroupMessage(event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, event.getId(), event.getTs());
+ } catch (e) {
+ if (e.name === "DecryptionError") {
+ // re-throw decryption errors as-is
+ throw e;
+ }
+ let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
+ if (e?.message === "OLM.UNKNOWN_MESSAGE_INDEX") {
+ this.requestKeysForEvent(event);
+ errorCode = "OLM_UNKNOWN_MESSAGE_INDEX";
+ }
+ throw new _base.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] || PROBLEM_DESCRIPTIONS.unknown;
+ if (problem.fixed) {
+ problemDescription += " Trying to create a new secure channel and re-requesting the keys.";
+ }
+ throw new _base.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, {
+ session: content.sender_key + "|" + content.session_id
+ });
+ }
+ throw new _base.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 _base.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
+ };
+ }
+ requestKeysForEvent(event) {
+ 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
+ *
+ */
+ addEventToPendingList(event) {
+ 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());
+ }
+ 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
+ *
+ */
+ removeEventFromPendingList(event) {
+ 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
+ *
+ */
+ roomKeyFromEvent(event) {
+ const senderKey = event.getSenderKey();
+ const content = event.getContent();
+ const extraSessionData = {};
+ 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 = {
+ 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
+ *
+ */
+ forwardedRoomKeyFromEvent(event) {
+ // 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();
+ 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
+ *
+ */
+ async shouldAcceptForwardedKey(event, roomKey) {
+ 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
+ *
+ */
+ async wasRoomKeyRequested(event, roomKey) {
+ // 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(), "*", [_OutgoingRoomKeyRequestManager.RoomKeyRequestState.Sent]);
+ return outgoingRequests.some(req => req.requestBody.room_id === roomKey.roomId && req.requestBody.session_id === roomKey.sessionId);
+ }
+ wasRoomKeyForwardedByInviter(event, roomKey) {
+ // 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;
+ }
+ }
+ wasRoomKeyForwardedAsHistory(roomKey) {
+ 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.
+ */
+ shouldParkForwardedKey(roomKey) {
+ 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
+ *
+ */
+ async parkForwardedKey(event, roomKey) {
+ 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.logger.withPrefix("[addParkedSharedHistory]"));
+ }
+
+ /**
+ * Add the given room key to our store.
+ *
+ * @param roomKey - The room key that should be added to the store.
+ *
+ * @internal
+ *
+ */
+ async addRoomKey(roomKey) {
+ 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
+ *
+ */
+ async onForwardedRoomKey(event) {
+ 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);
+ }
+ }
+ async onRoomKeyEvent(event) {
+ 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
+ */
+ async onRoomKeyWithheldEvent(event) {
+ 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);
+ }
+ }
+ async onNoOlmWithheldEvent(event) {
+ 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 = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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]])]]));
+ }
+ hasKeysForKeyRequest(keyRequest) {
+ const body = keyRequest.requestBody;
+ return this.olmDevice.hasInboundSessionKeys(body.room_id, body.sender_key, body.session_id
+ // TODO: ratchet index
+ );
+ }
+
+ shareKeysWithDevice(keyRequest) {
+ 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 = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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]])]]));
+ });
+ });
+ }
+ async buildKeyForwardingMessage(roomId, senderKey, sessionId) {
+ 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
+ */
+ importRoomKey(session, {
+ untrusted,
+ source
+ } = {}) {
+ const extraSessionData = {};
+ 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
+ */
+ async retryDecryption(senderKey, sessionId, forceRedecryptIfUntrusted) {
+ 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);
+ }
+ async retryDecryptionFromSender(senderKey) {
+ 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);
+ }
+ async sendSharedHistoryInboundSessions(devicesByUser) {
+ 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 = [];
+ const contentMap = new Map();
+ for (const [userId, devices] of devicesByUser) {
+ const deviceMessages = new Map();
+ contentMap.set(userId, deviceMessages);
+ for (const deviceInfo of devices) {
+ const encryptedContent = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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);
+ }
+ }
+}
+exports.MegolmDecryption = MegolmDecryption;
+const PROBLEM_DESCRIPTIONS = {
+ no_olm: "The sender was unable to establish a secure channel.",
+ unknown: "The secure channel with the sender was corrupted."
+};
+(0, _base.registerAlgorithm)(olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js
new file mode 100644
index 0000000000..6f72b95375
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js
@@ -0,0 +1,276 @@
+"use strict";
+
+var _logger = require("../../logger");
+var olmlib = _interopRequireWildcard(require("../olmlib"));
+var _deviceinfo = require("../deviceinfo");
+var _base = require("./base");
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification;
+/**
+ * Olm encryption implementation
+ *
+ * @param params - parameters, as per {@link EncryptionAlgorithm}
+ */
+class OlmEncryption extends _base.EncryptionAlgorithm {
+ constructor(...args) {
+ super(...args);
+ _defineProperty(this, "sessionPrepared", false);
+ _defineProperty(this, "prepPromise", null);
+ }
+ /**
+ * @internal
+ * @param roomMembers - list of currently-joined users in the room
+ * @returns Promise which resolves when setup is complete
+ */
+ ensureSession(roomMembers) {
+ 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
+ */
+ async encryptMessage(room, eventType, content) {
+ // 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 = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {}
+ };
+ const promises = [];
+ 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 _base.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.
+ */
+ async decryptEvent(event) {
+ const content = event.getWireContent();
+ const deviceKey = content.sender_key;
+ const ciphertext = content.ciphertext;
+ if (!ciphertext) {
+ throw new _base.DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext");
+ }
+ if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) {
+ throw new _base.DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients");
+ }
+ const message = ciphertext[this.olmDevice.deviceCurve25519Key];
+ let payloadString;
+ try {
+ payloadString = await this.decryptMessage(deviceKey, message);
+ } catch (e) {
+ throw new _base.DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", {
+ sender: deviceKey,
+ err: e
+ });
+ }
+ 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 _base.DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient);
+ }
+ if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
+ throw new _base.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.
+ //
+ // To do this, we need to make sure that our device list is up-to-date. If the device is unknown, we can only
+ // assume that the device logged out and accept it anyway. Some event handlers, such as secret sharing, may be
+ // more strict and reject events that come from unknown devices.
+ //
+ // This is a defence against the following scenario:
+ //
+ // * Alice has verified Bob and Mallory.
+ // * Mallory gets control of Alice's server, and sends a megolm session to Alice using her (Mallory's)
+ // senderkey, but claiming to be from Bob.
+ // * Mallory sends more events using that session, claiming to be from Bob.
+ // * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those
+ // events as verified even though the sender is forged.
+ //
+ // In practice, it's not clear that the js-sdk would behave that way, so this may be only a defence in depth.
+
+ await this.crypto.deviceList.downloadKeys([event.getSender()], false);
+ const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
+ if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
+ throw new _base.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 _base.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 _base.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.
+ */
+ decryptMessage(theirDeviceIdentityKey, message) {
+ // 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;
+ }
+ }
+ async reallyDecryptMessage(theirDeviceIdentityKey, message) {
+ const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
+
+ // try each session in turn.
+ const decryptionErrors = {};
+ for (const sessionId of sessionIds) {
+ try {
+ const payload = await this.olmDevice.decryptMessage(theirDeviceIdentityKey, sessionId, message.type, message.body);
+ _logger.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 + ": " + e.message);
+ }
+
+ // otherwise it's probably a message for another session; carry on, but
+ // keep a record of the error
+ decryptionErrors[sessionId] = 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;
+ try {
+ res = await this.olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body);
+ } catch (e) {
+ decryptionErrors["(new)"] = e.message;
+ throw new Error("Error decrypting prekey message: " + JSON.stringify(decryptionErrors));
+ }
+ _logger.logger.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey);
+ return res.payload;
+ }
+}
+(0, _base.registerAlgorithm)(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js
new file mode 100644
index 0000000000..aeed6bb466
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js
@@ -0,0 +1,12 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+Object.defineProperty(exports, "CrossSigningKey", {
+ enumerable: true,
+ get: function () {
+ return _cryptoApi.CrossSigningKey;
+ }
+});
+var _cryptoApi = require("../crypto-api"); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js
new file mode 100644
index 0000000000..554563213b
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js
@@ -0,0 +1,651 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.algorithmsByName = exports.DefaultAlgorithm = exports.Curve25519 = exports.BackupManager = exports.Aes256 = void 0;
+var _client = require("../client");
+var _logger = require("../logger");
+var _olmlib = require("./olmlib");
+var _key_passphrase = require("./key_passphrase");
+var _utils = require("../utils");
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+var _recoverykey = require("./recoverykey");
+var _aes = require("./aes");
+var _NamespacedValue = require("../NamespacedValue");
+var _index = require("./index");
+var _crypto = require("./crypto");
+var _httpApi = require("../http-api");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const KEY_BACKUP_KEYS_PER_REQUEST = 200;
+const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
+
+/* eslint-disable camelcase */
+
+/* eslint-enable camelcase */
+/** A function used to get the secret key for a backup.
+ */
+/**
+ * Manages the key backup.
+ */
+class BackupManager {
+ // When did we last try to check the server for a given session id?
+
+ constructor(baseApis, getKey) {
+ this.baseApis = baseApis;
+ this.getKey = getKey;
+ _defineProperty(this, "algorithm", void 0);
+ _defineProperty(this, "backupInfo", void 0);
+ // The info dict from /room_keys/version
+ _defineProperty(this, "checkedForBackup", void 0);
+ // Have we checked the server for a backup we can use?
+ _defineProperty(this, "sendingBackups", void 0);
+ // Are we currently sending backups?
+ _defineProperty(this, "sessionLastCheckAttemptedTime", {});
+ this.checkedForBackup = false;
+ this.sendingBackups = false;
+ }
+ get version() {
+ 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
+ */
+ static checkBackupVersion(info) {
+ 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);
+ }
+ static makeAlgorithm(info, getKey) {
+ const Algorithm = algorithmsByName[info.algorithm];
+ if (!Algorithm) {
+ throw new Error("Unknown backup algorithm");
+ }
+ return Algorithm.init(info.auth_data, getKey);
+ }
+ async enableKeyBackup(info) {
+ this.backupInfo = info;
+ if (this.algorithm) {
+ this.algorithm.free();
+ }
+ this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
+ this.baseApis.emit(_index.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.
+ */
+ disableKeyBackup() {
+ if (this.algorithm) {
+ this.algorithm.free();
+ }
+ this.algorithm = undefined;
+ this.backupInfo = undefined;
+ this.baseApis.emit(_index.CryptoEvent.KeyBackupStatus, false);
+ }
+ getKeyBackupEnabled() {
+ if (!this.checkedForBackup) {
+ return null;
+ }
+ return Boolean(this.algorithm);
+ }
+ async prepareKeyBackupVersion(key, algorithm) {
+ const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
+ if (!Algorithm) {
+ throw new Error("Unknown backup algorithm");
+ }
+ const [privateKey, authData] = await Algorithm.prepare(key);
+ const recoveryKey = (0, _recoverykey.encodeRecoveryKey)(privateKey);
+ return {
+ algorithm: Algorithm.algorithmName,
+ auth_data: authData,
+ recovery_key: recoveryKey,
+ privateKey
+ };
+ }
+ async createKeyBackupVersion(info) {
+ 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.
+ */
+ async checkAndStart() {
+ _logger.logger.log("Checking key backup status...");
+ if (this.baseApis.isGuest()) {
+ _logger.logger.log("Skipping key backup check since user is guest");
+ this.checkedForBackup = true;
+ return null;
+ }
+ let backupInfo;
+ try {
+ backupInfo = (await this.baseApis.getKeyBackupVersion()) ?? undefined;
+ } catch (e) {
+ _logger.logger.log("Error checking for active key backup", e);
+ if (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.logger.log(`Found usable key backup v${backupInfo.version}: enabling key backups`);
+ await this.enableKeyBackup(backupInfo);
+ } else if (!trustInfo.usable && this.backupInfo) {
+ _logger.logger.log("No usable key backup: disabling key backup");
+ this.disableKeyBackup();
+ } else if (!trustInfo.usable && !this.backupInfo) {
+ _logger.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.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.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.
+ */
+ async checkKeyBackup() {
+ 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.
+ */
+ async queryKeyBackupRateLimited(targetRoomId, targetSessionId) {
+ 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
+ */
+ async isKeyBackupTrusted(backupInfo) {
+ const ret = {
+ usable: false,
+ trusted_locally: false,
+ sigs: []
+ };
+ if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) {
+ _logger.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 = null;
+ try {
+ algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
+ if (await algorithm.keyMatches(privKey)) {
+ _logger.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.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 = {
+ 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 (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, userId, sigInfo.deviceId, crossSigningId);
+ sigInfo.valid = true;
+ } catch (e) {
+ _logger.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 (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, userId, device.deviceId, device.getFingerprint());
+ sigInfo.valid = true;
+ } catch (e) {
+ _logger.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.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.
+ */
+ async scheduleKeyBackupSend(maxDelay = 10000) {
+ 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 (0, _utils.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.logger.log("Key backup request failed", err);
+ if (err.data) {
+ if (err.data.errcode == "M_NOT_FOUND" || 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(_index.CryptoEvent.KeyBackupFailed, err.data.errcode);
+ throw err;
+ }
+ }
+ }
+ if (numFailures) {
+ // exponential backoff if we have failures
+ await (0, _utils.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
+ */
+ async backupPendingKeys(limit) {
+ 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(_index.CryptoEvent.KeyBackupSessionsRemaining, remaining);
+ const rooms = {};
+ for (const session of sessions) {
+ const roomId = session.sessionData.room_id;
+ (0, _utils.safeSet)(rooms, roomId, rooms[roomId] || {
+ sessions: {}
+ });
+ const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData);
+ sessionData.algorithm = _olmlib.MEGOLM_ALGORITHM;
+ const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length;
+ const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey(_olmlib.MEGOLM_ALGORITHM, session.senderKey);
+ const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(_olmlib.MEGOLM_ALGORITHM, session.senderKey) ?? undefined;
+ const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
+ (0, _utils.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(_index.CryptoEvent.KeyBackupSessionsRemaining, remaining);
+ return sessions.length;
+ }
+ async backupGroupSession(senderKey, sessionId) {
+ 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.
+ */
+ async scheduleAllGroupSessionsForBackup() {
+ 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).
+ */
+ async flagAllGroupSessionsForBackup() {
+ await this.baseApis.crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.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(_index.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
+ */
+ countSessionsNeedingBackup() {
+ return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
+ }
+}
+exports.BackupManager = BackupManager;
+class Curve25519 {
+ constructor(authData, publicKey,
+ // FIXME: PkEncryption
+ getKey) {
+ this.authData = authData;
+ this.publicKey = publicKey;
+ this.getKey = getKey;
+ }
+ static async init(authData, getKey) {
+ 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, publicKey, getKey);
+ }
+ static async prepare(key) {
+ const decryption = new global.Olm.PkDecryption();
+ try {
+ const authData = {};
+ 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 (0, _key_passphrase.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];
+ } finally {
+ decryption.free();
+ }
+ }
+ static checkBackupVersion(info) {
+ if (!("public_key" in info.auth_data)) {
+ throw new Error("Invalid backup data returned");
+ }
+ }
+ get untrusted() {
+ return true;
+ }
+ async encryptSession(data) {
+ const plainText = Object.assign({}, data);
+ delete plainText.session_id;
+ delete plainText.room_id;
+ delete plainText.first_known_index;
+ return this.publicKey.encrypt(JSON.stringify(plainText));
+ }
+ async decryptSessions(sessions) {
+ 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 _httpApi.MatrixError({
+ errcode: _client.MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY
+ });
+ }
+ const keys = [];
+ 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.logger.log("Failed to decrypt megolm session from backup", e, sessionData);
+ }
+ }
+ return keys;
+ } finally {
+ decryption.free();
+ }
+ }
+ async keyMatches(key) {
+ const decryption = new global.Olm.PkDecryption();
+ let pubKey;
+ try {
+ pubKey = decryption.init_with_private_key(key);
+ } finally {
+ decryption.free();
+ }
+ return pubKey === this.authData.public_key;
+ }
+ free() {
+ this.publicKey.free();
+ }
+}
+exports.Curve25519 = Curve25519;
+_defineProperty(Curve25519, "algorithmName", "m.megolm_backup.v1.curve25519-aes-sha2");
+function randomBytes(size) {
+ const buf = new Uint8Array(size);
+ _crypto.crypto.getRandomValues(buf);
+ return buf;
+}
+const UNSTABLE_MSC3270_NAME = new _NamespacedValue.UnstableValue("m.megolm_backup.v1.aes-hmac-sha2", "org.matrix.msc3270.v1.aes-hmac-sha2");
+class Aes256 {
+ constructor(authData, key) {
+ this.authData = authData;
+ this.key = key;
+ }
+ static async init(authData, getKey) {
+ if (!authData) {
+ throw new Error("auth_data missing");
+ }
+ const key = await getKey();
+ if (authData.mac) {
+ const {
+ mac
+ } = await (0, _aes.calculateKeyCheck)(key, authData.iv);
+ if (authData.mac.replace(/=+$/g, "") !== mac.replace(/=+/g, "")) {
+ throw new Error("Key does not match");
+ }
+ }
+ return new Aes256(authData, key);
+ }
+ static async prepare(key) {
+ let outKey;
+ const authData = {};
+ if (!key) {
+ outKey = randomBytes(32);
+ } else if (key instanceof Uint8Array) {
+ outKey = new Uint8Array(key);
+ } else {
+ const derivation = await (0, _key_passphrase.keyFromPassphrase)(key);
+ authData.private_key_salt = derivation.salt;
+ authData.private_key_iterations = derivation.iterations;
+ outKey = derivation.key;
+ }
+ const {
+ iv,
+ mac
+ } = await (0, _aes.calculateKeyCheck)(outKey);
+ authData.iv = iv;
+ authData.mac = mac;
+ return [outKey, authData];
+ }
+ static checkBackupVersion(info) {
+ if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
+ throw new Error("Invalid backup data returned");
+ }
+ }
+ get untrusted() {
+ return false;
+ }
+ encryptSession(data) {
+ const plainText = Object.assign({}, data);
+ delete plainText.session_id;
+ delete plainText.room_id;
+ delete plainText.first_known_index;
+ return (0, _aes.encryptAES)(JSON.stringify(plainText), this.key, data.session_id);
+ }
+ async decryptSessions(sessions) {
+ const keys = [];
+ for (const [sessionId, sessionData] of Object.entries(sessions)) {
+ try {
+ const decrypted = JSON.parse(await (0, _aes.decryptAES)(sessionData.session_data, this.key, sessionId));
+ decrypted.session_id = sessionId;
+ keys.push(decrypted);
+ } catch (e) {
+ _logger.logger.log("Failed to decrypt megolm session from backup", e, sessionData);
+ }
+ }
+ return keys;
+ }
+ async keyMatches(key) {
+ if (this.authData.mac) {
+ const {
+ mac
+ } = await (0, _aes.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;
+ }
+ }
+ free() {
+ this.key.fill(0);
+ }
+}
+exports.Aes256 = Aes256;
+_defineProperty(Aes256, "algorithmName", UNSTABLE_MSC3270_NAME.name);
+const algorithmsByName = {
+ [Curve25519.algorithmName]: Curve25519,
+ [Aes256.algorithmName]: Aes256
+};
+exports.algorithmsByName = algorithmsByName;
+const DefaultAlgorithm = Curve25519;
+exports.DefaultAlgorithm = DefaultAlgorithm; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js
new file mode 100644
index 0000000000..f4a47c9ca7
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js
@@ -0,0 +1,60 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.crypto = exports.TextEncoder = void 0;
+exports.setCrypto = setCrypto;
+exports.setTextEncoder = setTextEncoder;
+exports.subtleCrypto = void 0;
+var _logger = require("../logger");
+/*
+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.
+*/
+
+let crypto = global.window?.crypto;
+exports.crypto = crypto;
+let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
+exports.subtleCrypto = subtleCrypto;
+let TextEncoder = global.window?.TextEncoder;
+
+/* eslint-disable @typescript-eslint/no-var-requires */
+exports.TextEncoder = TextEncoder;
+if (!crypto) {
+ try {
+ exports.crypto = crypto = require("crypto").webcrypto;
+ } catch (e) {
+ _logger.logger.error("Failed to load webcrypto", e);
+ }
+}
+if (!subtleCrypto) {
+ exports.subtleCrypto = subtleCrypto = crypto?.subtle;
+}
+if (!TextEncoder) {
+ try {
+ exports.TextEncoder = TextEncoder = require("util").TextEncoder;
+ } catch (e) {
+ _logger.logger.error("Failed to load TextEncoder util", e);
+ }
+}
+/* eslint-enable @typescript-eslint/no-var-requires */
+
+function setCrypto(_crypto) {
+ exports.crypto = crypto = _crypto;
+ exports.subtleCrypto = subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle;
+}
+function setTextEncoder(_TextEncoder) {
+ exports.TextEncoder = TextEncoder = _TextEncoder;
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js
new file mode 100644
index 0000000000..8ee568ae8c
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js
@@ -0,0 +1,237 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DehydrationManager = exports.DEHYDRATION_ALGORITHM = void 0;
+var _anotherJson = _interopRequireDefault(require("another-json"));
+var _olmlib = require("./olmlib");
+var _indexeddbCryptoStore = require("../crypto/store/indexeddb-crypto-store");
+var _aes = require("./aes");
+var _logger = require("../logger");
+var _httpApi = require("../http-api");
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
+exports.DEHYDRATION_ALGORITHM = DEHYDRATION_ALGORITHM;
+const oneweek = 7 * 24 * 60 * 60 * 1000;
+class DehydrationManager {
+ constructor(crypto) {
+ this.crypto = crypto;
+ _defineProperty(this, "inProgress", false);
+ _defineProperty(this, "timeoutId", void 0);
+ _defineProperty(this, "key", void 0);
+ _defineProperty(this, "keyInfo", void 0);
+ _defineProperty(this, "deviceDisplayName", void 0);
+ this.getDehydrationKeyFromCache();
+ }
+ getDehydrationKeyFromCache() {
+ return this.crypto.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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 (0, _aes.decryptAES)(key, pickleKey, DEHYDRATION_ALGORITHM);
+ this.key = (0, _olmlib.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 */
+ async setKeyAndQueueDehydration(key, keyInfo = {}, deviceDisplayName) {
+ const matches = await this.setKey(key, keyInfo, deviceDisplayName);
+ if (!matches) {
+ // start dehydration in the background
+ this.dehydrateDevice();
+ }
+ }
+ async setKey(key, keyInfo = {}, deviceDisplayName) {
+ 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.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 = !!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 */
+ async dehydrateDevice() {
+ if (this.inProgress) {
+ _logger.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 (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(this.key), pickleKey, DEHYDRATION_ALGORITHM);
+ await this.crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
+ this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
+ keyInfo: this.keyInfo,
+ key,
+ deviceDisplayName: this.deviceDisplayName,
+ time: Date.now()
+ });
+ });
+ _logger.logger.log("Attempting to dehydrate device");
+ _logger.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 = JSON.parse(account.one_time_keys());
+ const fallbacks = 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 = {
+ algorithm: DEHYDRATION_ALGORITHM,
+ account: pickledAccount
+ };
+ if (this.keyInfo.passphrase) {
+ deviceData.passphrase = this.keyInfo.passphrase;
+ }
+ _logger.logger.log("Uploading account to server");
+ // eslint-disable-next-line camelcase
+ const dehydrateResult = await this.crypto.baseApis.http.authedRequest(_httpApi.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.logger.log("Preparing device keys", deviceId);
+ const deviceKeys = {
+ 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.default.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.logger.log("Preparing one-time keys");
+ const oneTimeKeys = {};
+ for (const [keyId, key] of Object.entries(otks.curve25519)) {
+ const k = {
+ key
+ };
+ const signature = account.sign(_anotherJson.default.stringify(k));
+ k.signatures = {
+ [this.crypto.userId]: {
+ [`ed25519:${deviceId}`]: signature
+ }
+ };
+ oneTimeKeys[`signed_curve25519:${keyId}`] = k;
+ }
+ _logger.logger.log("Preparing fallback keys");
+ const fallbackKeys = {};
+ for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
+ const k = {
+ key,
+ fallback: true
+ };
+ const signature = account.sign(_anotherJson.default.stringify(k));
+ k.signatures = {
+ [this.crypto.userId]: {
+ [`ed25519:${deviceId}`]: signature
+ }
+ };
+ fallbackKeys[`signed_curve25519:${keyId}`] = k;
+ }
+ _logger.logger.log("Uploading keys to server");
+ await this.crypto.baseApis.http.authedRequest(_httpApi.Method.Post, "/keys/upload/" + encodeURI(deviceId), undefined, {
+ "device_keys": deviceKeys,
+ "one_time_keys": oneTimeKeys,
+ "org.matrix.msc2732.fallback_keys": fallbackKeys
+ });
+ _logger.logger.log("Done dehydrating");
+
+ // dehydrate again in a week
+ this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek);
+ return deviceId;
+ } finally {
+ this.inProgress = false;
+ }
+ }
+ stop() {
+ if (this.timeoutId) {
+ global.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ }
+ }
+}
+exports.DehydrationManager = DehydrationManager; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/device-converter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/device-converter.js
new file mode 100644
index 0000000000..9a14d49d66
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/device-converter.js
@@ -0,0 +1,47 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.deviceInfoToDevice = deviceInfoToDevice;
+var _device = require("../models/device");
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * Convert a {@link DeviceInfo} to a {@link Device}.
+ * @param deviceInfo - deviceInfo to convert
+ * @param userId - id of the user that owns the device.
+ */
+function deviceInfoToDevice(deviceInfo, userId) {
+ const keys = new Map(Object.entries(deviceInfo.keys));
+ const displayName = deviceInfo.getDisplayName() || undefined;
+ const signatures = new Map();
+ if (deviceInfo.signatures) {
+ for (const userId in deviceInfo.signatures) {
+ signatures.set(userId, new Map(Object.entries(deviceInfo.signatures[userId])));
+ }
+ }
+ return new _device.Device({
+ deviceId: deviceInfo.deviceId,
+ userId: userId,
+ keys,
+ algorithms: deviceInfo.algorithms,
+ verified: deviceInfo.verified,
+ signatures,
+ displayName
+ });
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js
new file mode 100644
index 0000000000..7dc2035303
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js
@@ -0,0 +1,152 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DeviceInfo = void 0;
+var _device = require("../models/device");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * Information about a user's device
+ */
+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
+ */
+ static fromStorage(obj, deviceId) {
+ 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] = obj[prop];
+ }
+ }
+ return res;
+ }
+ /**
+ * @param deviceId - id of the device
+ */
+ constructor(deviceId) {
+ this.deviceId = deviceId;
+ /** list of algorithms supported by this device */
+ _defineProperty(this, "algorithms", []);
+ /** a map from `<key type>:<id> -> <base64-encoded key>` */
+ _defineProperty(this, "keys", {});
+ /** whether the device has been verified/blocked by the user */
+ _defineProperty(this, "verified", _device.DeviceVerification.Unverified);
+ /**
+ * whether the user knows of this device's existence
+ * (useful when warning the user that a user has added new devices)
+ */
+ _defineProperty(this, "known", false);
+ /** additional data from the homeserver */
+ _defineProperty(this, "unsigned", {});
+ _defineProperty(this, "signatures", {});
+ }
+
+ /**
+ * Prepare a DeviceInfo for JSON serialisation in the session store
+ *
+ * @returns deviceinfo with non-serialised members removed
+ */
+ toStorage() {
+ 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
+ */
+ getFingerprint() {
+ 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
+ */
+ getIdentityKey() {
+ return this.keys["curve25519:" + this.deviceId];
+ }
+
+ /**
+ * Get the configured display name for this device, if any
+ *
+ * @returns displayname
+ */
+ getDisplayName() {
+ return this.unsigned.device_display_name || null;
+ }
+
+ /**
+ * Returns true if this device is blocked
+ *
+ * @returns true if blocked
+ */
+ isBlocked() {
+ return this.verified == _device.DeviceVerification.Blocked;
+ }
+
+ /**
+ * Returns true if this device is verified
+ *
+ * @returns true if verified
+ */
+ isVerified() {
+ return this.verified == _device.DeviceVerification.Verified;
+ }
+
+ /**
+ * Returns true if this device is unverified
+ *
+ * @returns true if unverified
+ */
+ isUnverified() {
+ return this.verified == _device.DeviceVerification.Unverified;
+ }
+
+ /**
+ * Returns true if the user knows about this device's existence
+ *
+ * @returns true if known
+ */
+ isKnown() {
+ return this.known === true;
+ }
+}
+exports.DeviceInfo = DeviceInfo;
+_defineProperty(DeviceInfo, "DeviceVerification", {
+ VERIFIED: _device.DeviceVerification.Verified,
+ UNVERIFIED: _device.DeviceVerification.Unverified,
+ BLOCKED: _device.DeviceVerification.Blocked
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js
new file mode 100644
index 0000000000..7d1a5a202c
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js
@@ -0,0 +1,3427 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.IncomingRoomKeyRequest = exports.CryptoEvent = exports.Crypto = void 0;
+exports.fixBackupKey = fixBackupKey;
+exports.isCryptoAvailable = isCryptoAvailable;
+exports.verificationMethods = void 0;
+var _anotherJson = _interopRequireDefault(require("another-json"));
+var _uuid = require("uuid");
+var _event = require("../@types/event");
+var _ReEmitter = require("../ReEmitter");
+var _logger = require("../logger");
+var _OlmDevice = require("./OlmDevice");
+var olmlib = _interopRequireWildcard(require("./olmlib"));
+var _DeviceList = require("./DeviceList");
+var _deviceinfo = require("./deviceinfo");
+var algorithms = _interopRequireWildcard(require("./algorithms"));
+var _CrossSigning = require("./CrossSigning");
+var _EncryptionSetup = require("./EncryptionSetup");
+var _SecretStorage = require("./SecretStorage");
+var _api = require("./api");
+var _OutgoingRoomKeyRequestManager = require("./OutgoingRoomKeyRequestManager");
+var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
+var _QRCode = require("./verification/QRCode");
+var _SAS = require("./verification/SAS");
+var _key_passphrase = require("./key_passphrase");
+var _recoverykey = require("./recoverykey");
+var _VerificationRequest = require("./verification/request/VerificationRequest");
+var _InRoomChannel = require("./verification/request/InRoomChannel");
+var _ToDeviceChannel = require("./verification/request/ToDeviceChannel");
+var _IllegalMethod = require("./verification/IllegalMethod");
+var _errors = require("../errors");
+var _aes = require("./aes");
+var _dehydration = require("./dehydration");
+var _backup = require("./backup");
+var _room = require("../models/room");
+var _roomMember = require("../models/room-member");
+var _event2 = require("../models/event");
+var _client = require("../client");
+var _typedEventEmitter = require("../models/typed-event-emitter");
+var _roomState = require("../models/room-state");
+var _utils = require("../utils");
+var _secretStorage = require("../secret-storage");
+var _deviceConverter = require("./device-converter");
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/* re-exports for backwards compatibility */
+
+const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification;
+const defaultVerificationMethods = {
+ [_QRCode.ReciprocateQRCode.NAME]: _QRCode.ReciprocateQRCode,
+ [_SAS.SAS.NAME]: _SAS.SAS,
+ // 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.
+ [_QRCode.SHOW_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod,
+ [_QRCode.SCAN_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod
+};
+
+/**
+ * verification method names
+ */
+// legacy export identifier
+const verificationMethods = {
+ RECIPROCATE_QR_CODE: _QRCode.ReciprocateQRCode.NAME,
+ SAS: _SAS.SAS.NAME
+};
+exports.verificationMethods = verificationMethods;
+function isCryptoAvailable() {
+ return Boolean(global.Olm);
+}
+const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
+
+/* eslint-disable camelcase */
+
+/**
+ * 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.
+ */
+
+/* eslint-enable camelcase */
+
+/* eslint-disable camelcase */
+
+/* eslint-enable camelcase */
+let CryptoEvent = /*#__PURE__*/function (CryptoEvent) {
+ CryptoEvent["DeviceVerificationChanged"] = "deviceVerificationChanged";
+ CryptoEvent["UserTrustStatusChanged"] = "userTrustStatusChanged";
+ CryptoEvent["UserCrossSigningUpdated"] = "userCrossSigningUpdated";
+ CryptoEvent["RoomKeyRequest"] = "crypto.roomKeyRequest";
+ CryptoEvent["RoomKeyRequestCancellation"] = "crypto.roomKeyRequestCancellation";
+ CryptoEvent["KeyBackupStatus"] = "crypto.keyBackupStatus";
+ CryptoEvent["KeyBackupFailed"] = "crypto.keyBackupFailed";
+ CryptoEvent["KeyBackupSessionsRemaining"] = "crypto.keyBackupSessionsRemaining";
+ CryptoEvent["KeySignatureUploadFailure"] = "crypto.keySignatureUploadFailure";
+ CryptoEvent["VerificationRequest"] = "crypto.verification.request";
+ CryptoEvent["Warning"] = "crypto.warning";
+ CryptoEvent["WillUpdateDevices"] = "crypto.willUpdateDevices";
+ CryptoEvent["DevicesUpdated"] = "crypto.devicesUpdated";
+ CryptoEvent["KeysChanged"] = "crossSigning.keysChanged";
+ return CryptoEvent;
+}({});
+exports.CryptoEvent = CryptoEvent;
+class Crypto extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * @returns The version of Olm.
+ */
+ static getOlmVersion() {
+ return _OlmDevice.OlmDevice.getOlmVersion();
+ }
+ /**
+ * 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.
+ */
+ constructor(baseApis, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) {
+ super();
+ this.baseApis = baseApis;
+ this.userId = userId;
+ this.deviceId = deviceId;
+ this.clientStore = clientStore;
+ this.cryptoStore = cryptoStore;
+ this.roomList = roomList;
+ _defineProperty(this, "backupManager", void 0);
+ _defineProperty(this, "crossSigningInfo", void 0);
+ _defineProperty(this, "olmDevice", void 0);
+ _defineProperty(this, "deviceList", void 0);
+ _defineProperty(this, "dehydrationManager", void 0);
+ _defineProperty(this, "secretStorage", void 0);
+ _defineProperty(this, "reEmitter", void 0);
+ _defineProperty(this, "verificationMethods", void 0);
+ _defineProperty(this, "supportedAlgorithms", void 0);
+ _defineProperty(this, "outgoingRoomKeyRequestManager", void 0);
+ _defineProperty(this, "toDeviceVerificationRequests", void 0);
+ _defineProperty(this, "inRoomVerificationRequests", void 0);
+ _defineProperty(this, "trustCrossSignedDevices", true);
+ // the last time we did a check for the number of one-time-keys on the server.
+ _defineProperty(this, "lastOneTimeKeyCheck", null);
+ _defineProperty(this, "oneTimeKeyCheckInProgress", false);
+ // EncryptionAlgorithm instance for each room
+ _defineProperty(this, "roomEncryptors", new Map());
+ // map from algorithm to DecryptionAlgorithm instance, for each room
+ _defineProperty(this, "roomDecryptors", new Map());
+ _defineProperty(this, "deviceKeys", {});
+ // type: key
+ _defineProperty(this, "globalBlacklistUnverifiedDevices", false);
+ _defineProperty(this, "globalErrorOnUnknownDevices", true);
+ // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
+ // we received in the current sync.
+ _defineProperty(this, "receivedRoomKeyRequests", []);
+ _defineProperty(this, "receivedRoomKeyRequestCancellations", []);
+ // true if we are currently processing received room key requests
+ _defineProperty(this, "processingRoomKeyRequests", false);
+ // controls whether device tracking is delayed
+ // until calling encryptEvent or trackRoomDevices,
+ // or done immediately upon enabling room encryption.
+ _defineProperty(this, "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.
+ _defineProperty(this, "roomDeviceTrackingState", {});
+ // 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
+ _defineProperty(this, "lastNewSessionForced", new _utils.MapWithDefault(() => new _utils.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.
+ _defineProperty(this, "sendKeyRequestsImmediately", false);
+ _defineProperty(this, "oneTimeKeyCount", void 0);
+ _defineProperty(this, "needsNewFallback", void 0);
+ _defineProperty(this, "fallbackCleanup", void 0);
+ /*
+ * Event handler for DeviceList's userNewDevices event
+ */
+ _defineProperty(this, "onDeviceListUserCrossSigningUpdated", async userId => {
+ 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));
+ }
+ });
+ _defineProperty(this, "onMembership", (event, member, oldMembership) => {
+ try {
+ this.onRoomMembership(event, member, oldMembership);
+ } catch (e) {
+ _logger.logger.error("Error handling membership change:", e);
+ }
+ });
+ _defineProperty(this, "onToDeviceEvent", event => {
+ try {
+ _logger.logger.log(`received to-device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getContent()[_event.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(_event2.MatrixEventEvent.Decrypted, ev => {
+ this.onToDeviceEvent(ev);
+ });
+ }
+ } catch (e) {
+ _logger.logger.error("Error handling toDeviceEvent:", e);
+ }
+ });
+ /**
+ * 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
+ */
+ _defineProperty(this, "onTimelineEvent", (event, room, atStart, removed, {
+ liveEvent = true
+ } = {}) => {
+ if (!_InRoomChannel.InRoomChannel.validateEvent(event, this.baseApis)) {
+ return;
+ }
+ const createRequest = event => {
+ const channel = new _InRoomChannel.InRoomChannel(this.baseApis, event.getRoomId());
+ return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis);
+ };
+ this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
+ });
+ this.reEmitter = new _ReEmitter.TypedReEmitter(this);
+ if (verificationMethods) {
+ this.verificationMethods = new Map();
+ for (const method of verificationMethods) {
+ if (typeof method === "string") {
+ if (defaultVerificationMethods[method]) {
+ this.verificationMethods.set(method, defaultVerificationMethods[method]);
+ }
+ } else if (method["NAME"]) {
+ this.verificationMethods.set(method["NAME"], method);
+ } else {
+ _logger.logger.warn(`Excluding unknown verification method ${method}`);
+ }
+ }
+ } else {
+ this.verificationMethods = new Map(Object.entries(defaultVerificationMethods));
+ }
+ this.backupManager = new _backup.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.secretStorage.get("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.secretStorage.getKey();
+ await this.secretStorage.store("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.OlmDevice(cryptoStore);
+ this.deviceList = new _DeviceList.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.OutgoingRoomKeyRequestManager(baseApis, this.deviceId, this.cryptoStore);
+ this.toDeviceVerificationRequests = new _ToDeviceChannel.ToDeviceRequests();
+ this.inRoomVerificationRequests = new _InRoomChannel.InRoomRequests();
+ const cryptoCallbacks = this.baseApis.cryptoCallbacks || {};
+ const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(cryptoStore, this.olmDevice);
+ this.crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
+ // Yes, we pass the client twice here: see SecretStorage
+ this.secretStorage = new _SecretStorage.SecretStorage(baseApis, cryptoCallbacks, baseApis);
+ this.dehydrationManager = new _dehydration.DehydrationManager(this);
+
+ // Assuming no app-supplied callback, default to getting from SSSS.
+ if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
+ cryptoCallbacks.getCrossSigningKey = async type => {
+ return _CrossSigning.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.
+ */
+ async init({
+ exportedOlmDevice,
+ pickleKey
+ } = {}) {
+ _logger.logger.log("Crypto: initialising Olm...");
+ await global.Olm.init();
+ _logger.logger.log(exportedOlmDevice ? "Crypto: initialising Olm device from exported device..." : "Crypto: initialising Olm device...");
+ await this.olmDevice.init({
+ fromExportedDevice: exportedOlmDevice,
+ pickleKey
+ });
+ _logger.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.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.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.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.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.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
+ */
+ getTrustCrossSignedDevices() {
+ return this.trustCrossSignedDevices;
+ }
+
+ /**
+ * @deprecated Use {@link Crypto.CryptoApi#getTrustCrossSignedDevices}.
+ */
+ getCryptoTrustCrossSignedDevices() {
+ return this.trustCrossSignedDevices;
+ }
+
+ /**
+ * See getCryptoTrustCrossSignedDevices
+ *
+ * @param val - True to trust cross-signed devices
+ */
+ setTrustCrossSignedDevices(val) {
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * @deprecated Use {@link Crypto.CryptoApi#setTrustCrossSignedDevices}.
+ */
+ setCryptoTrustCrossSignedDevices(val) {
+ this.setTrustCrossSignedDevices(val);
+ }
+
+ /**
+ * 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.
+ */
+ async createRecoveryKeyFromPassphrase(password) {
+ const decryption = new global.Olm.PkDecryption();
+ try {
+ const keyInfo = {};
+ if (password) {
+ const derivation = await (0, _key_passphrase.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 = (0, _recoverykey.encodeRecoveryKey)(privateKey);
+ return {
+ keyInfo: 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
+ */
+ async userHasCrossSigningKeys() {
+ 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
+ */
+ async isCrossSigningReady() {
+ 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
+ */
+ async isSecretStorageReady() {
+ 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.
+ */
+ async bootstrapCrossSigning({
+ authUploadDeviceSigningKeys,
+ setupNewCrossSigning
+ } = {}) {
+ _logger.logger.log("Bootstrapping cross-signing");
+ const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
+ const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
+ const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this.userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks);
+
+ // Reset the cross-signing keys
+ const resetCrossSigning = async () => {
+ 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.logger.log({
+ setupNewCrossSigning,
+ publicKeysOnDevice,
+ privateKeysInCache,
+ privateKeysInStorage,
+ privateKeysExistSomewhere
+ });
+ if (!privateKeysExistSomewhere || setupNewCrossSigning) {
+ _logger.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.logger.log("Cross-signing public keys trusted and private keys found locally");
+ } else if (privateKeysInStorage) {
+ _logger.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.ServerSideSecretStorageImpl(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks);
+ if (await secretStorage.hasKey()) {
+ _logger.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 _CrossSigning.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.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
+ async bootstrapSecretStorage({
+ createSecretStorageKey = async () => ({}),
+ keyBackupInfo,
+ setupNewKeyBackup,
+ setupNewSecretStorage,
+ getKeyBackupPassphrase
+ } = {}) {
+ _logger.logger.log("Bootstrapping Secure Secret Storage");
+ const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
+ const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
+ const secretStorage = new _secretStorage.ServerSideSecretStorageImpl(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks);
+
+ // the ID of the new SSSS key, if we create one
+ let newKeyId = null;
+
+ // create a new SSSS key and set it as default
+ const createSSSS = async (opts, privateKey) => {
+ if (privateKey) {
+ opts.key = privateKey;
+ }
+ const {
+ keyId,
+ keyInfo
+ } = await secretStorage.addKey(_secretStorage.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, keyInfo) => {
+ 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 (0, _aes.calculateKeyCheck)(privateKey);
+ keyInfo.iv = iv;
+ keyInfo.mac = mac;
+ await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
+ }
+ }
+ };
+ const signKeyBackupWithCrossSigning = async keyBackupAuthData => {
+ if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) {
+ try {
+ _logger.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.logger.error("Signing key backup with cross-signing keys failed", e);
+ }
+ } else {
+ _logger.logger.warn("Cross-signing keys not available, skipping signature on key backup");
+ }
+ };
+ const oldSSSSKey = await this.secretStorage.getKey();
+ const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
+ const storageExists = !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === _secretStorage.SECRET_STORAGE_ALGORITHM_V1_AES;
+
+ // Log all relevant state for easier parsing of debug logs.
+ _logger.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.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 = {},
+ privateKey
+ } = await createSecretStorageKey();
+ newKeyId = await createSSSS(keyInfo, privateKey);
+ } else if (!storageExists && keyBackupInfo) {
+ // we have an existing backup, but no SSSS
+ _logger.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 = {};
+ 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.logger.log("Secret storage exists");
+ if (oldKeyInfo && oldKeyInfo.algorithm === _secretStorage.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.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 _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
+ }
+ if (setupNewKeyBackup && !keyBackupInfo) {
+ _logger.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 = (0, _recoverykey.decodeRecoveryKey)(info.recovery_key);
+ await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
+
+ // create keyBackupInfo object to add to builder
+ const data = {
+ 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.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.logger.error("Key backup is enabled but couldn't get key backup key!");
+ return;
+ }
+ _logger.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.logger.log("Secure Secret Storage ready");
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#addKey}.
+ */
+ addSecretStorageKey(algorithm, opts, keyID) {
+ return this.secretStorage.addKey(algorithm, opts, keyID);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#hasKey}.
+ */
+ hasSecretStorageKey(keyID) {
+ return this.secretStorage.hasKey(keyID);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#getKey}.
+ */
+ getSecretStorageKey(keyID) {
+ return this.secretStorage.getKey(keyID);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}.
+ */
+ storeSecret(name, secret, keys) {
+ return this.secretStorage.store(name, secret, keys);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}.
+ */
+ getSecret(name) {
+ return this.secretStorage.get(name);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
+ */
+ isSecretStored(name) {
+ return this.secretStorage.isStored(name);
+ }
+ requestSecret(name, devices) {
+ if (!devices) {
+ devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
+ }
+ return this.secretStorage.request(name, devices);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#getDefaultKeyId}.
+ */
+ getDefaultSecretStorageKeyId() {
+ return this.secretStorage.getDefaultKeyId();
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#setDefaultKeyId}.
+ */
+ setDefaultSecretStorageKeyId(k) {
+ return this.secretStorage.setDefaultKeyId(k);
+ }
+
+ /**
+ * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#checkKey}.
+ */
+ checkSecretStorageKey(key, info) {
+ 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
+ */
+ checkSecretStoragePrivateKey(privateKey, expectedPublicKey) {
+ let decryption = 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
+ */
+ async getSessionBackupPrivateKey() {
+ let key = await new Promise(resolve => {
+ // TODO types
+ this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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 (0, _aes.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
+ */
+ async storeSessionBackupPrivateKey(key) {
+ 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 (0, _aes.encryptAES)(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
+ return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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
+ */
+ checkCrossSigningPrivateKey(privateKey, expectedPublicKey) {
+ let signing = 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.
+ */
+ async afterCrossSigningLocalKeyChange() {
+ _logger.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.logger.info(`Starting background key sig upload for ${this.deviceId}`);
+ const upload = ({
+ shouldEmit = false
+ }) => {
+ 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 _errors.KeySignatureUploadError("Key upload failed", {
+ failures
+ });
+ }
+ _logger.logger.info(`Finished background key sig upload for ${this.deviceId}`);
+ }).catch(e => {
+ _logger.logger.error(`Error during background key sig upload for ${this.deviceId}`, e);
+ });
+ };
+ upload({
+ shouldEmit: true
+ });
+ const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
+ if (shouldUpgradeCb) {
+ _logger.logger.info("Starting device verification upgrade");
+
+ // Check all users for signatures if upgrade callback present
+ // FIXME: do this in batches
+ const users = {};
+ for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) {
+ const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, _CrossSigning.CrossSigningInfo.fromStorage(crossSigningInfo, userId));
+ if (upgradeInfo) {
+ users[userId] = upgradeInfo;
+ }
+ }
+ if (Object.keys(users).length > 0) {
+ _logger.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.logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e);
+ }
+ }
+ _logger.logger.info("Finished device verification upgrade");
+ }
+ _logger.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
+ */
+ async checkForDeviceVerificationUpgrade(userId, crossSigningInfo) {
+ // 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.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
+ */
+ async checkForValidDeviceSignature(userId, key, devices) {
+ const deviceIds = [];
+ 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
+ */
+ getCrossSigningKeyId(type = _api.CrossSigningKey.Master) {
+ return Promise.resolve(this.getCrossSigningId(type));
+ }
+
+ // old name, for backwards compatibility
+ getCrossSigningId(type) {
+ 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.
+ */
+ getStoredCrossSigningForUser(userId) {
+ return this.deviceList.getStoredCrossSigningForUser(userId);
+ }
+
+ /**
+ * Check whether a given user is trusted.
+ *
+ * @param userId - The ID of the user to check.
+ *
+ * @returns
+ */
+ checkUserTrust(userId) {
+ const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
+ if (!userCrossSigning) {
+ return new _CrossSigning.UserTrustLevel(false, false, false);
+ }
+ return this.crossSigningInfo.checkUserTrust(userCrossSigning);
+ }
+
+ /**
+ * Check whether a given device is trusted.
+ *
+ * @param userId - The ID of the user whose device is to be checked.
+ * @param deviceId - The ID of the device to check
+ */
+ async getDeviceVerificationStatus(userId, deviceId) {
+ const device = this.deviceList.getStoredDevice(userId, deviceId);
+ if (!device) {
+ return null;
+ }
+ return this.checkDeviceInfoTrust(userId, device);
+ }
+
+ /**
+ * @deprecated Use {@link Crypto.CryptoApi.getDeviceVerificationStatus}.
+ */
+ checkDeviceTrust(userId, deviceId) {
+ 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
+ *
+ * @deprecated Use {@link Crypto.CryptoApi.getDeviceVerificationStatus}.
+ */
+ checkDeviceInfoTrust(userId, device) {
+ 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 _CrossSigning.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
+ */
+ checkIfOwnDeviceCrossSigned(deviceId) {
+ 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;
+ }
+ /**
+ * 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.
+ */
+ async checkOwnCrossSigningTrust({
+ allowPrivateKeyRequests = false
+ } = {}) {
+ 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.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.logger.info("Got new master public key", seenPubkey);
+ }
+ if (allowPrivateKeyRequests && (masterChanged || masterExistsNotLocallyCached)) {
+ _logger.logger.info("Attempting to retrieve cross-signing master private key");
+ let signing = 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.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 = {};
+ if (selfSigningChanged) {
+ _logger.logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
+ }
+ if (allowPrivateKeyRequests && (selfSigningChanged || selfSigningExistsNotLocallyCached)) {
+ _logger.logger.info("Attempting to retrieve cross-signing self-signing private key");
+ let signing = null;
+ try {
+ const ret = await this.crossSigningInfo.getCrossSigningKey("self_signing", newCrossSigning.getId("self_signing"));
+ signing = ret[1];
+ _logger.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.logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
+ }
+ if (allowPrivateKeyRequests && (userSigningChanged || userSigningExistsNotLocallyCached)) {
+ _logger.logger.info("Attempting to retrieve cross-signing user-signing private key");
+ let signing = null;
+ try {
+ const ret = await this.crossSigningInfo.getCrossSigningKey("user_signing", newCrossSigning.getId("user_signing"));
+ signing = ret[1];
+ _logger.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({}, masterKey, {
+ signatures: {
+ [this.userId]: {
+ ["ed25519:" + this.deviceId]: deviceSig
+ }
+ }
+ });
+ }
+ const keysToUpload = Object.keys(keySignatures);
+ if (keysToUpload.length) {
+ const upload = ({
+ shouldEmit = false
+ }) => {
+ _logger.logger.info(`Starting background key sig upload for ${keysToUpload}`);
+ return this.baseApis.uploadKeySignatures({
+ [this.userId]: keySignatures
+ }).then(response => {
+ const {
+ failures
+ } = response || {};
+ _logger.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 _errors.KeySignatureUploadError("Key upload failed", {
+ failures
+ });
+ }
+ }).catch(e => {
+ _logger.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
+ */
+ async storeTrustedSelfKeys(keys) {
+ if (keys) {
+ this.crossSigningInfo.setKeys(keys);
+ } else {
+ this.crossSigningInfo.clearKeys();
+ }
+ await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.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
+ */
+ async checkDeviceVerifications(userId) {
+ const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
+ if (!shouldUpgradeCb) {
+ // Upgrading skipped when callback is not present.
+ return;
+ }
+ _logger.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.logger.info(`Finished device verification upgrade for ${userId}`);
+ }
+
+ /**
+ */
+ enableLazyLoading() {
+ 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
+ */
+ registerEventHandlers(eventEmitter) {
+ eventEmitter.on(_roomMember.RoomMemberEvent.Membership, this.onMembership);
+ eventEmitter.on(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
+ eventEmitter.on(_room.RoomEvent.Timeline, this.onTimelineEvent);
+ eventEmitter.on(_event2.MatrixEventEvent.Decrypted, this.onTimelineEvent);
+ }
+
+ /**
+ * @deprecated this does nothing and will be removed in a future version
+ */
+ start() {
+ _logger.logger.warn("MatrixClient.crypto.start() is deprecated");
+ }
+
+ /** Stop background processes related to crypto */
+ stop() {
+ this.outgoingRoomKeyRequestManager.stop();
+ this.deviceList.stop();
+ this.dehydrationManager.stop();
+ }
+
+ /**
+ * Get the Ed25519 key for this device
+ *
+ * @returns base64-encoded ed25519 key.
+ */
+ getDeviceEd25519Key() {
+ return this.olmDevice.deviceEd25519Key;
+ }
+
+ /**
+ * Get the Curve25519 key for this device
+ *
+ * @returns base64-encoded curve25519 key.
+ */
+ getDeviceCurve25519Key() {
+ 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 Set {@link Crypto.CryptoApi#globalBlacklistUnverifiedDevices | CryptoApi.globalBlacklistUnverifiedDevices} directly.
+ */
+ setGlobalBlacklistUnverifiedDevices(value) {
+ this.globalBlacklistUnverifiedDevices = value;
+ }
+
+ /**
+ * @returns whether to blacklist all unverified devices by default
+ *
+ * @deprecated Reference {@link Crypto.CryptoApi#globalBlacklistUnverifiedDevices | CryptoApi.globalBlacklistUnverifiedDevices} directly.
+ */
+ getGlobalBlacklistUnverifiedDevices() {
+ return this.globalBlacklistUnverifiedDevices;
+ }
+
+ /**
+ * Upload the device keys to the homeserver.
+ * @returns A promise that will resolve when the keys are uploaded.
+ */
+ uploadDeviceKeys() {
+ 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
+ });
+ });
+ }
+ getNeedsNewFallback() {
+ return !!this.needsNewFallback;
+ }
+
+ // check if it's time to upload one-time keys, and do so if so.
+ maybeUploadOneTimeKeys() {
+ // 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 => {
+ while (keyLimit > keyCount || this.getNeedsNewFallback()) {
+ // Ask olm to generate new one time keys, then upload them to synapse.
+ if (keyLimit > keyCount) {
+ _logger.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.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.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.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
+ async uploadOneTimeKeys() {
+ const promises = [];
+ let fallbackJson;
+ 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.needsNewFallback = false;
+ }
+ const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
+ const oneTimeJson = {};
+ 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 = {
+ 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}`.
+ */
+ downloadKeys(userIds, forceDownload) {
+ 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.
+ */
+ getStoredDevicesForUser(userId) {
+ return this.deviceList.getStoredDevicesForUser(userId);
+ }
+
+ /**
+ * Get the device information for the given list of users.
+ *
+ * @param userIds - The users to fetch.
+ * @param downloadUncached - If true, download the device list for users whose device list we are not
+ * currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
+ *
+ * @returns A map `{@link DeviceMap}`.
+ */
+ async getUserDeviceInfo(userIds, downloadUncached = false) {
+ const deviceMapByUserId = new Map();
+ // Keep the users without device to download theirs keys
+ const usersWithoutDeviceInfo = [];
+ for (const userId of userIds) {
+ const deviceInfos = await this.getStoredDevicesForUser(userId);
+ // If there are device infos for a userId, we transform it into a map
+ // Else, the keys will be downloaded after
+ if (deviceInfos) {
+ const deviceMap = new Map(
+ // Convert DeviceInfo to Device
+ deviceInfos.map(deviceInfo => [deviceInfo.deviceId, (0, _deviceConverter.deviceInfoToDevice)(deviceInfo, userId)]));
+ deviceMapByUserId.set(userId, deviceMap);
+ } else {
+ usersWithoutDeviceInfo.push(userId);
+ }
+ }
+
+ // Download device info for users without device infos
+ if (downloadUncached && usersWithoutDeviceInfo.length > 0) {
+ const newDeviceInfoMap = await this.downloadKeys(usersWithoutDeviceInfo);
+ newDeviceInfoMap.forEach((deviceInfoMap, userId) => {
+ const deviceMap = new Map();
+ // Convert DeviceInfo to Device
+ deviceInfoMap.forEach((deviceInfo, deviceId) => deviceMap.set(deviceId, (0, _deviceConverter.deviceInfoToDevice)(deviceInfo, userId)));
+
+ // Put the new device infos into the returned map
+ deviceMapByUserId.set(userId, deviceMap);
+ });
+ }
+ return deviceMapByUserId;
+ }
+
+ /**
+ * Get the stored keys for a single device
+ *
+ *
+ * @returns device, or undefined
+ * if we don't know about this device
+ */
+ getStoredDevice(userId, deviceId) {
+ 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.
+ */
+ saveDeviceList(delay) {
+ 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
+ */
+ async setDeviceVerification(userId, deviceId, verified = null, blocked = null, known = null, keys) {
+ // 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.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
+ }) => {
+ _logger.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 _errors.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; // 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.logger.info("Own device " + deviceId + " marked verified: signing");
+
+ // Signing only needed if other device not already signed
+ let device;
+ const deviceTrust = this.checkDeviceTrust(userId, deviceId);
+ if (deviceTrust.isCrossSigningVerified()) {
+ _logger.logger.log(`Own device ${deviceId} already cross-signing verified`);
+ } else {
+ device = await this.crossSigningInfo.signDevice(userId, _deviceinfo.DeviceInfo.fromStorage(dev, deviceId));
+ }
+ if (device) {
+ const upload = async ({
+ shouldEmit = false
+ }) => {
+ _logger.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 _errors.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.DeviceInfo.fromStorage(dev, deviceId);
+ this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
+ return deviceObj;
+ }
+ findVerificationRequestDMInProgress(roomId) {
+ return this.inRoomVerificationRequests.findRequestInProgress(roomId);
+ }
+ getVerificationRequestsToDeviceInProgress(userId) {
+ return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
+ }
+ requestVerificationDM(userId, roomId) {
+ const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
+ if (existingRequest) {
+ return Promise.resolve(existingRequest);
+ }
+ const channel = new _InRoomChannel.InRoomChannel(this.baseApis, roomId, userId);
+ return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
+ }
+ requestVerification(userId, devices) {
+ 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.ToDeviceChannel(this.baseApis, userId, devices, _ToDeviceChannel.ToDeviceChannel.makeTransactionId());
+ return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
+ }
+ async requestVerificationWithChannel(userId, channel, requestsMap) {
+ let request = new _VerificationRequest.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.logger.log(`Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`);
+ requestsMap.setRequestByChannel(channel, request);
+ }
+ return request;
+ }
+ beginKeyVerification(method, userId, deviceId, transactionId = null) {
+ let request;
+ 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.ToDeviceChannel.makeTransactionId();
+ const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
+ request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis);
+ this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
+ }
+ return request.beginKeyVerification(method, {
+ userId,
+ deviceId
+ });
+ }
+ async legacyDeviceVerification(userId, deviceId, method) {
+ const transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId();
+ const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
+ const request = new _VerificationRequest.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
+ */
+ async getOlmSessionsForUser(userId) {
+ const devices = this.getStoredDevicesForUser(userId) || [];
+ const result = {};
+ 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
+ */
+ getEventSenderDeviceInfo(event) {
+ 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.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
+ return null;
+ }
+ if (claimedKey !== device.getFingerprint()) {
+ _logger.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)
+ */
+ getEventEncryptionInfo(event) {
+ const ret = {};
+ ret.senderKey = event.getSenderKey() ?? undefined;
+ ret.algorithm = event.getWireContent().algorithm;
+ if (!ret.senderKey || !ret.algorithm) {
+ ret.encrypted = false;
+ return ret;
+ }
+ 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.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
+ ret.mismatchedSender = true;
+ }
+ if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
+ _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + ret.sender.getFingerprint());
+ ret.mismatchedSender = true;
+ }
+ return ret;
+ }
+
+ /**
+ * 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.
+ */
+ forceDiscardSession(roomId) {
+ 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).
+ */
+ async setRoomEncryption(roomId, config, inhibitDeviceQuery) {
+ 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.
+ */
+ async setRoomEncryptionImpl(room, config) {
+ 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.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.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 = 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.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 => {
+ room.off(_roomState.RoomStateEvent.Update, onState);
+ if (room.membersLoaded()) {
+ this.trackRoomDevicesImpl(room).catch(e => {
+ _logger.logger.error(`Error enabling device tracking in ${roomId}`, e);
+ });
+ }
+ };
+ room.on(_roomState.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.
+ */
+ trackRoomDevices(roomId) {
+ 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
+ */
+ trackRoomDevicesImpl(room) {
+ const roomId = room.roomId;
+ const trackMembers = async () => {
+ // not an encrypted room
+ if (!this.roomEncryptors.has(roomId)) {
+ return;
+ }
+ _logger.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
+ * `IOlmSessionResult`
+ */
+ ensureOlmSessionsForUsers(users, force) {
+ // map user Id → DeviceInfo[]
+ const devicesByUser = new Map();
+ for (const userId of users) {
+ const userDevices = [];
+ 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
+ */
+ async exportRoomKeys() {
+ const exportedSessions = [];
+ await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.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
+ */
+ importRoomKeys(keys, opts = {}) {
+ let successes = 0;
+ let failures = 0;
+ const total = keys.length;
+ function updateProgress() {
+ opts.progressCallback?.({
+ stage: "load_keys",
+ successes,
+ failures,
+ total
+ });
+ }
+ return Promise.all(keys.map(key => {
+ if (!key.room_id || !key.algorithm) {
+ _logger.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
+ */
+ countSessionsNeedingBackup() {
+ 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
+ */
+ prepareToEncrypt(room) {
+ 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
+ */
+ async encryptEvent(event, room) {
+ 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);
+ 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.
+ */
+ async decryptEvent(event) {
+ 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 _event2.MatrixEvent(_objectSpread({
+ room_id: event.getRoomId()
+ }, event.getUnsigned().redacted_because));
+ let redactedBecause = event.getUnsigned().redacted_because;
+ if (redactionEvent.isEncrypted()) {
+ try {
+ const decryptedEvent = await this.decryptEvent(redactionEvent);
+ redactedBecause = decryptedEvent.clearEvent;
+ } catch (e) {
+ _logger.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 that device lists have
+ * been changed.
+ *
+ * @param deviceLists - device_lists field from /sync
+ */
+ async processDeviceLists(deviceLists) {
+ // 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(deviceLists);
+ }
+
+ /**
+ * 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
+ */
+ requestRoomKey(requestBody, recipients, resend = false) {
+ 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.logger.error("Error requesting key for event", e);
+ });
+ }
+
+ /**
+ * Cancel any earlier room key request
+ *
+ * @param requestBody - parameters to match for cancellation
+ */
+ cancelRoomKeyRequest(requestBody) {
+ this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch(e => {
+ _logger.logger.warn("Error clearing pending room key requests", e);
+ });
+ }
+
+ /**
+ * Re-send any outgoing key requests, eg after verification
+ * @returns
+ */
+ async cancelAndResendAllOutgoingKeyRequests() {
+ 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
+ */
+ async onCryptoEvent(room, event) {
+ const content = event.getContent();
+ await this.setRoomEncryptionImpl(room, content);
+ }
+
+ /**
+ * Called before the result of a sync is processed
+ *
+ * @param syncData - the data from the 'MatrixClient.sync' event
+ */
+ async onSyncWillProcess(syncData) {
+ 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.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
+ */
+ async onSyncCompleted(syncData) {
+ 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
+ */
+ async evalDeviceListChanges(deviceLists) {
+ 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
+ */
+ async getTrackedE2eUsers() {
+ const e2eUserIds = [];
+ 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
+ */
+ getTrackedE2eRooms() {
+ 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.
+ */
+ async encryptAndSendToDevices(userDeviceInfoArr, payload) {
+ const toDeviceBatch = {
+ eventType: _event.EventType.RoomMessageEncrypted,
+ batch: []
+ };
+ try {
+ await Promise.all(userDeviceInfoArr.map(async ({
+ userId,
+ deviceInfo
+ }) => {
+ const deviceId = deviceInfo.deviceId;
+ const encryptedContent = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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.logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
+ return false;
+ }
+ });
+ try {
+ await this.baseApis.queueToDevice(toDeviceBatch);
+ } catch (e) {
+ _logger.logger.error("sendToDevice failed", e);
+ throw e;
+ }
+ } catch (e) {
+ _logger.logger.error("encryptAndSendToDevices promises failed", e);
+ throw e;
+ }
+ }
+ async preprocessToDeviceMessages(events) {
+ // 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 === _event.EventType.RoomMessageEncrypted && !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)) {
+ _logger.logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
+ return false;
+ }
+ return true;
+ });
+ }
+
+ /**
+ * Stores the current one_time_key count which will be handled later (in a call of
+ * onSyncCompleted).
+ *
+ * @param currentCount - The current count of one_time_keys to be stored
+ */
+ updateOneTimeKeyCount(currentCount) {
+ if (isFinite(currentCount)) {
+ this.oneTimeKeyCount = currentCount;
+ } else {
+ throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number");
+ }
+ }
+ processKeyCounts(oneTimeKeysCounts, unusedFallbackKeys) {
+ if (oneTimeKeysCounts !== undefined) {
+ this.updateOneTimeKeyCount(oneTimeKeysCounts["signed_curve25519"] || 0);
+ }
+ if (unusedFallbackKeys !== undefined) {
+ // If `unusedFallbackKeys` is defined, that means `device_unused_fallback_key_types`
+ // is present in the sync response, which indicates that the server supports fallback keys.
+ //
+ // If there's no unused signed_curve25519 fallback key, we need a new one.
+ this.needsNewFallback = !unusedFallbackKeys.includes("signed_curve25519");
+ }
+ return Promise.resolve();
+ }
+ /**
+ * Handle a key event
+ *
+ * @internal
+ * @param event - key event
+ */
+ onRoomKeyEvent(event) {
+ const content = event.getContent();
+ if (!content.room_id || !content.algorithm) {
+ _logger.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
+ */
+ onRoomKeyWithheldEvent(event) {
+ const content = event.getContent();
+ if (content.code !== "m.no_olm" && (!content.room_id || !content.session_id) || !content.algorithm || !content.sender_key) {
+ _logger.logger.error("key withheld event is missing fields");
+ return;
+ }
+ _logger.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
+ */
+ onKeyVerificationMessage(event) {
+ if (!_ToDeviceChannel.ToDeviceChannel.validateEvent(event, this.baseApis)) {
+ return;
+ }
+ const createRequest = event => {
+ if (!_ToDeviceChannel.ToDeviceChannel.canCreateRequest(_ToDeviceChannel.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.ToDeviceChannel(this.baseApis, userId, [deviceId]);
+ return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis);
+ };
+ this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
+ }
+ async handleVerificationEvent(event, requestsMap, createRequest, isLiveEvent = true) {
+ // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
+ if (event.isSending() && event.status != _event2.EventStatus.SENT) {
+ let eventIdListener;
+ let statusListener;
+ try {
+ await new Promise((resolve, reject) => {
+ eventIdListener = resolve;
+ statusListener = () => {
+ if (event.status == _event2.EventStatus.CANCELLED) {
+ reject(new Error("Event status set to CANCELLED."));
+ }
+ };
+ event.once(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
+ event.on(_event2.MatrixEventEvent.Status, statusListener);
+ });
+ } catch (err) {
+ _logger.logger.error("error while waiting for the verification event to be sent: ", err);
+ return;
+ } finally {
+ event.removeListener(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
+ event.removeListener(_event2.MatrixEventEvent.Status, statusListener);
+ }
+ }
+ let request = 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.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.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
+ */
+ async onToDeviceBadEncrypted(event) {
+ const content = event.getWireContent();
+ const sender = event.getSender();
+ const algorithm = content.algorithm;
+ const deviceKey = content.sender_key;
+ this.baseApis.emit(_client.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 = () => {
+ 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.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.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 = {
+ algorithm: olmlib.OLM_ALGORITHM,
+ sender_key: this.olmDevice.deviceCurve25519Key,
+ ciphertext: {},
+ [_event.ToDeviceMessageId]: (0, _uuid.v4)()
+ };
+ 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
+ */
+ onRoomMembership(event, member, oldMembership) {
+ // 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.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.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
+ */
+ onRoomKeyRequestEvent(event) {
+ 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
+ */
+ async processReceivedRoomKeyRequests() {
+ 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.logger.error(`Error processing room key requsts: ${e}`);
+ } finally {
+ this.processingRoomKeyRequests = false;
+ }
+ }
+
+ /**
+ * Helper for processReceivedRoomKeyRequests
+ *
+ */
+ async processReceivedRoomKeyRequest(req) {
+ const userId = req.userId;
+ const deviceId = req.deviceId;
+ const body = req.requestBody;
+ const roomId = body.room_id;
+ const alg = body.algorithm;
+ _logger.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.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.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.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.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.logger.log(`room key request for unencrypted room ${roomId}`);
+ return;
+ }
+ const decryptor = this.roomDecryptors.get(roomId).get(alg);
+ if (!decryptor) {
+ _logger.logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
+ return;
+ }
+ if (!(await decryptor.hasKeysForKeyRequest(req))) {
+ _logger.logger.log(`room key request for unknown session ${roomId} / ` + body.session_id);
+ return;
+ }
+ req.share = () => {
+ decryptor.shareKeysWithDevice(req);
+ };
+
+ // if the device is verified already, share the keys
+ if (this.checkDeviceTrust(userId, deviceId).isVerified()) {
+ _logger.logger.log("device is already verified: sharing keys");
+ req.share();
+ return;
+ }
+ this.emit(CryptoEvent.RoomKeyRequest, req);
+ }
+
+ /**
+ * Helper for processReceivedRoomKeyRequests
+ *
+ */
+ async processReceivedRoomKeyRequestCancellation(cancellation) {
+ _logger.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 `DecryptionError` if the algorithm is unknown
+ */
+ getRoomDecryptor(roomId, algorithm) {
+ let decryptors;
+ let alg;
+ if (roomId) {
+ decryptors = this.roomDecryptors.get(roomId);
+ if (!decryptors) {
+ decryptors = new Map();
+ 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
+ */
+ getRoomDecryptors(algorithm) {
+ const decryptors = [];
+ 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
+ */
+ async signObject(obj) {
+ 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.default.stringify(obj));
+ obj.signatures = (0, _utils.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.
+ *
+ */
+exports.Crypto = Crypto;
+function fixBackupKey(key) {
+ 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
+ */
+class IncomingRoomKeyRequest {
+ constructor(event) {
+ /** user requesting the key */
+ _defineProperty(this, "userId", void 0);
+ /** device requesting the key */
+ _defineProperty(this, "deviceId", void 0);
+ /** unique id for the request */
+ _defineProperty(this, "requestId", void 0);
+ _defineProperty(this, "requestBody", void 0);
+ /**
+ * callback which, when called, will ask
+ * the relevant crypto algorithm implementation to share the keys for
+ * this request.
+ */
+ _defineProperty(this, "share", void 0);
+ 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 = () => {
+ throw new Error("don't know how to share keys for this request yet");
+ };
+ }
+}
+
+/**
+ * Represents a received m.room_key_request cancellation
+ */
+exports.IncomingRoomKeyRequest = IncomingRoomKeyRequest;
+class IncomingRoomKeyRequestCancellation {
+ constructor(event) {
+ /** user requesting the cancellation */
+ _defineProperty(this, "userId", void 0);
+ /** device requesting the cancellation */
+ _defineProperty(this, "deviceId", void 0);
+ /** unique id for the request to be cancelled */
+ _defineProperty(this, "requestId", void 0);
+ 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. \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js
new file mode 100644
index 0000000000..3f4d3bbcba
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js
@@ -0,0 +1,69 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.deriveKey = deriveKey;
+exports.keyFromAuthData = keyFromAuthData;
+exports.keyFromPassphrase = keyFromPassphrase;
+var _randomstring = require("../randomstring");
+var _crypto = require("./crypto");
+/*
+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.
+*/
+
+const DEFAULT_ITERATIONS = 500000;
+const DEFAULT_BITSIZE = 256;
+
+/* eslint-disable camelcase */
+
+/* eslint-enable camelcase */
+
+function keyFromAuthData(authData, password) {
+ 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);
+}
+async function keyFromPassphrase(password) {
+ if (!global.Olm) {
+ throw new Error("Olm is not available");
+ }
+ const salt = (0, _randomstring.randomString)(32);
+ const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
+ return {
+ key,
+ salt,
+ iterations: DEFAULT_ITERATIONS
+ };
+}
+async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
+ if (!_crypto.subtleCrypto || !_crypto.TextEncoder) {
+ throw new Error("Password-based backup is not available on this platform");
+ }
+ const key = await _crypto.subtleCrypto.importKey("raw", new _crypto.TextEncoder().encode(password), {
+ name: "PBKDF2"
+ }, false, ["deriveBits"]);
+ const keybits = await _crypto.subtleCrypto.deriveBits({
+ name: "PBKDF2",
+ salt: new _crypto.TextEncoder().encode(salt),
+ iterations: iterations,
+ hash: "SHA-512"
+ }, key, numBits);
+ return new Uint8Array(keybits);
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/keybackup.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/keybackup.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/keybackup.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js
new file mode 100644
index 0000000000..ea397f0c0e
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js
@@ -0,0 +1,480 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.OLM_ALGORITHM = exports.MEGOLM_BACKUP_ALGORITHM = exports.MEGOLM_ALGORITHM = void 0;
+exports.decodeBase64 = decodeBase64;
+exports.encodeBase64 = encodeBase64;
+exports.encodeUnpaddedBase64 = encodeUnpaddedBase64;
+exports.encryptMessageForDevice = encryptMessageForDevice;
+exports.ensureOlmSessionsForDevices = ensureOlmSessionsForDevices;
+exports.getExistingOlmSessions = getExistingOlmSessions;
+exports.isOlmEncrypted = isOlmEncrypted;
+exports.pkSign = pkSign;
+exports.pkVerify = pkVerify;
+exports.verifySignature = verifySignature;
+var _anotherJson = _interopRequireDefault(require("another-json"));
+var _logger = require("../logger");
+var _event = require("../@types/event");
+var _utils = require("../utils");
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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
+ */
+var Algorithm = /*#__PURE__*/function (Algorithm) {
+ Algorithm["Olm"] = "m.olm.v1.curve25519-aes-sha2";
+ Algorithm["Megolm"] = "m.megolm.v1.aes-sha2";
+ Algorithm["MegolmBackup"] = "m.megolm_backup.v1.curve25519-aes-sha2";
+ return Algorithm;
+}(Algorithm || {});
+/**
+ * matrix algorithm tag for olm
+ */
+const OLM_ALGORITHM = Algorithm.Olm;
+
+/**
+ * matrix algorithm tag for megolm
+ */
+exports.OLM_ALGORITHM = OLM_ALGORITHM;
+const MEGOLM_ALGORITHM = Algorithm.Megolm;
+
+/**
+ * matrix algorithm tag for megolm backups
+ */
+exports.MEGOLM_ALGORITHM = MEGOLM_ALGORITHM;
+const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
+exports.MEGOLM_BACKUP_ALGORITHM = MEGOLM_BACKUP_ALGORITHM;
+/**
+ * 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`
+ */
+async function encryptMessageForDevice(resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields) {
+ 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.logger.log(`[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` + `${recipientUserId}:${recipientDevice.deviceId}`);
+ return;
+ }
+ _logger.logger.log(`[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` + `${recipientUserId}:${recipientDevice.deviceId}`);
+ const payload = _objectSpread({
+ 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));
+}
+/**
+ * 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}
+ */
+async function getExistingOlmSessions(olmDevice, baseApis, devicesByUser) {
+ // map user Id → DeviceInfo[]
+ const devicesWithoutSession = new _utils.MapWithDefault(() => []);
+ // map user Id → device Id → IExistingOlmSession
+ const sessions = new _utils.MapWithDefault(() => new Map());
+ const promises = [];
+ for (const [userId, devices] of Object.entries(devicesByUser)) {
+ for (const deviceInfo of devices) {
+ const deviceId = deviceInfo.deviceId;
+ const key = deviceInfo.getIdentityKey();
+ promises.push((async () => {
+ 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}
+ */
+async function ensureOlmSessionsForDevices(olmDevice, baseApis, devicesByUser, force = false, otkTimeout, failedServers, log = _logger.logger) {
+ const devicesWithoutSession = [
+ // [userId, deviceId], ...
+ ];
+ // map user Id → device Id → IExistingOlmSession
+ const result = new Map();
+ // map device key → resolve session fn
+ const resolveSession = 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 => {
+ 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;
+ 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 || {};
+ const promises = [];
+ 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 = 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, oneTimeKey, userId, deviceInfo) {
+ const deviceId = deviceInfo.deviceId;
+ try {
+ await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint());
+ } catch (e) {
+ _logger.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.logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e);
+ return null;
+ }
+ _logger.logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId);
+ return sid;
+}
+/**
+ * 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.
+ */
+async function verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) {
+ 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.default.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
+ */
+function pkSign(obj, key, userId, pubKey) {
+ 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.default.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
+ */
+function pkVerify(obj, pubKey, userId) {
+ 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.default.stringify(obj), signature);
+ } finally {
+ obj.signatures = sigs;
+ if (unsigned) obj.unsigned = unsigned;
+ util.free();
+ }
+}
+
+/**
+ * Check that an event was encrypted using olm.
+ */
+function isOlmEncrypted(event) {
+ if (!event.getSenderKey()) {
+ _logger.logger.error("Event has no sender key (not encrypted?)");
+ return false;
+ }
+ if (event.getWireType() !== _event.EventType.RoomMessageEncrypted || !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)) {
+ _logger.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.
+ */
+function encodeBase64(uint8Array) {
+ 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.
+ */
+function encodeUnpaddedBase64(uint8Array) {
+ 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.
+ */
+function decodeBase64(base64) {
+ return Buffer.from(base64, "base64");
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js
new file mode 100644
index 0000000000..a2a75618cb
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js
@@ -0,0 +1,60 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.decodeRecoveryKey = decodeRecoveryKey;
+exports.encodeRecoveryKey = encodeRecoveryKey;
+var bs58 = _interopRequireWildcard(require("bs58"));
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+/*
+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.
+*/
+
+// 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];
+function encodeRecoveryKey(key) {
+ 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(" ");
+}
+function decodeRecoveryKey(recoveryKey) {
+ 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));
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/base.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/base.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/base.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js
new file mode 100644
index 0000000000..e2d77f8af7
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js
@@ -0,0 +1,913 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.VERSION = exports.Backend = void 0;
+exports.upgradeDatabase = upgradeDatabase;
+var _logger = require("../../logger");
+var _utils = require("../../utils");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+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.
+ */
+class Backend {
+ /**
+ */
+ constructor(db) {
+ this.db = db;
+ _defineProperty(this, "nextTxnId", 0);
+ // 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 = () => {
+ _logger.logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
+ db.close();
+ };
+ }
+ async startup() {
+ // No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
+ // by passing us a ready IDBDatabase instance
+ return this;
+ }
+ async deleteAllData() {
+ 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.
+ */
+ getOrAddOutgoingRoomKeyRequest(request) {
+ 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.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.logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
+ txn.oncomplete = () => {
+ 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
+ */
+ getOutgoingRoomKeyRequest(requestBody) {
+ 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
+ _getOutgoingRoomKeyRequest(txn, requestBody, callback) {
+ const store = txn.objectStore("outgoingRoomKeyRequests");
+ const idx = store.index("session");
+ const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);
+ cursorReq.onsuccess = () => {
+ const cursor = cursorReq.result;
+ if (!cursor) {
+ // no match found
+ callback(null);
+ return;
+ }
+ const existing = cursor.value;
+ if ((0, _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.
+ */
+ getOutgoingRoomKeyRequestByState(wantedStates) {
+ 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;
+ function onsuccess() {
+ 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.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
+ */
+ getAllOutgoingRoomKeyRequestsByState(wantedState) {
+ 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 = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ }
+ getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
+ let stateIndex = 0;
+ const results = [];
+ function onsuccess() {
+ const cursor = this.result;
+ if (cursor) {
+ const keyReq = cursor.value;
+ if (keyReq.recipients.some(recipient => 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.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
+ */
+ updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
+ let result = null;
+ function onsuccess() {
+ const cursor = this.result;
+ if (!cursor) {
+ return;
+ }
+ const data = cursor.value;
+ if (data.state != expectedState) {
+ _logger.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
+ */
+ deleteOutgoingRoomKeyRequest(requestId, expectedState) {
+ const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
+ const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
+ cursorReq.onsuccess = () => {
+ const cursor = cursorReq.result;
+ if (!cursor) {
+ return;
+ }
+ const data = cursor.value;
+ if (data.state != expectedState) {
+ _logger.logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`);
+ return;
+ }
+ cursor.delete();
+ };
+ return promiseifyTxn(txn);
+ }
+
+ // Olm Account
+
+ getAccount(txn, func) {
+ const objectStore = txn.objectStore("account");
+ const getReq = objectStore.get("-");
+ getReq.onsuccess = function () {
+ try {
+ func(getReq.result || null);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ storeAccount(txn, accountPickle) {
+ const objectStore = txn.objectStore("account");
+ objectStore.put(accountPickle, "-");
+ }
+ getCrossSigningKeys(txn, func) {
+ const objectStore = txn.objectStore("account");
+ const getReq = objectStore.get("crossSigningKeys");
+ getReq.onsuccess = function () {
+ try {
+ func(getReq.result || null);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ getSecretStorePrivateKey(txn, func, type) {
+ const objectStore = txn.objectStore("account");
+ const getReq = objectStore.get(`ssss_cache:${type}`);
+ getReq.onsuccess = function () {
+ try {
+ func(getReq.result || null);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ storeCrossSigningKeys(txn, keys) {
+ const objectStore = txn.objectStore("account");
+ objectStore.put(keys, "crossSigningKeys");
+ }
+ storeSecretStorePrivateKey(txn, type, key) {
+ const objectStore = txn.objectStore("account");
+ objectStore.put(key, `ssss_cache:${type}`);
+ }
+
+ // Olm Sessions
+
+ countEndToEndSessions(txn, func) {
+ const objectStore = txn.objectStore("sessions");
+ const countReq = objectStore.count();
+ countReq.onsuccess = function () {
+ try {
+ func(countReq.result);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ getEndToEndSessions(deviceKey, txn, func) {
+ const objectStore = txn.objectStore("sessions");
+ const idx = objectStore.index("deviceKey");
+ const getReq = idx.openCursor(deviceKey);
+ const results = {};
+ getReq.onsuccess = function () {
+ 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, e);
+ }
+ }
+ };
+ }
+ getEndToEndSession(deviceKey, sessionId, txn, func) {
+ const objectStore = txn.objectStore("sessions");
+ const getReq = objectStore.get([deviceKey, sessionId]);
+ getReq.onsuccess = function () {
+ try {
+ if (getReq.result) {
+ func({
+ session: getReq.result.session,
+ lastReceivedMessageTs: getReq.result.lastReceivedMessageTs
+ });
+ } else {
+ func(null);
+ }
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ getAllEndToEndSessions(txn, func) {
+ const objectStore = txn.objectStore("sessions");
+ const getReq = objectStore.openCursor();
+ getReq.onsuccess = function () {
+ try {
+ const cursor = getReq.result;
+ if (cursor) {
+ func(cursor.value);
+ cursor.continue();
+ } else {
+ func(null);
+ }
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+ const objectStore = txn.objectStore("sessions");
+ objectStore.put({
+ deviceKey,
+ sessionId,
+ session: sessionInfo.session,
+ lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs
+ });
+ }
+ async storeEndToEndSessionProblem(deviceKey, type, fixed) {
+ 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);
+ }
+ async getEndToEndSessionProblem(deviceKey, timestamp) {
+ let result = 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 = () => {
+ 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
+ async filterOutNotifiedErrorDevices(devices) {
+ const txn = this.db.transaction("notified_error_devices", "readwrite");
+ const objectStore = txn.objectStore("notified_error_devices");
+ const ret = [];
+ await Promise.all(devices.map(device => {
+ return new Promise(resolve => {
+ const {
+ userId,
+ deviceInfo
+ } = device;
+ const getReq = objectStore.get([userId, deviceInfo.deviceId]);
+ getReq.onsuccess = function () {
+ if (!getReq.result) {
+ objectStore.put({
+ userId,
+ deviceId: deviceInfo.deviceId
+ });
+ ret.push(device);
+ }
+ resolve();
+ };
+ });
+ }));
+ return ret;
+ }
+
+ // Inbound group sessions
+
+ getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
+ let session = false;
+ let withheld = false;
+ const objectStore = txn.objectStore("inbound_group_sessions");
+ const getReq = objectStore.get([senderCurve25519Key, sessionId]);
+ getReq.onsuccess = function () {
+ try {
+ if (getReq.result) {
+ session = getReq.result.session;
+ } else {
+ session = null;
+ }
+ if (withheld !== false) {
+ func(session, withheld);
+ }
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
+ const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
+ withheldGetReq.onsuccess = function () {
+ try {
+ if (withheldGetReq.result) {
+ withheld = withheldGetReq.result.session;
+ } else {
+ withheld = null;
+ }
+ if (session !== false) {
+ func(session, withheld);
+ }
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ getAllEndToEndInboundGroupSessions(txn, func) {
+ const objectStore = txn.objectStore("inbound_group_sessions");
+ const getReq = objectStore.openCursor();
+ getReq.onsuccess = function () {
+ const cursor = getReq.result;
+ if (cursor) {
+ try {
+ func({
+ senderKey: cursor.value.senderCurve25519Key,
+ sessionId: cursor.value.sessionId,
+ sessionData: cursor.value.session
+ });
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ cursor.continue();
+ } else {
+ try {
+ func(null);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ }
+ };
+ }
+ addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ const objectStore = txn.objectStore("inbound_group_sessions");
+ const addReq = objectStore.add({
+ senderCurve25519Key,
+ sessionId,
+ session: sessionData
+ });
+ addReq.onerror = ev => {
+ 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.logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
+ } else {
+ abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
+ }
+ };
+ }
+ storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ const objectStore = txn.objectStore("inbound_group_sessions");
+ objectStore.put({
+ senderCurve25519Key,
+ sessionId,
+ session: sessionData
+ });
+ }
+ storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) {
+ const objectStore = txn.objectStore("inbound_group_sessions_withheld");
+ objectStore.put({
+ senderCurve25519Key,
+ sessionId,
+ session: sessionData
+ });
+ }
+ getEndToEndDeviceData(txn, func) {
+ const objectStore = txn.objectStore("device_data");
+ const getReq = objectStore.get("-");
+ getReq.onsuccess = function () {
+ try {
+ func(getReq.result || null);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ };
+ }
+ storeEndToEndDeviceData(deviceData, txn) {
+ const objectStore = txn.objectStore("device_data");
+ objectStore.put(deviceData, "-");
+ }
+ storeEndToEndRoom(roomId, roomInfo, txn) {
+ const objectStore = txn.objectStore("rooms");
+ objectStore.put(roomInfo, roomId);
+ }
+ getEndToEndRooms(txn, func) {
+ const rooms = {};
+ const objectStore = txn.objectStore("rooms");
+ const getReq = objectStore.openCursor();
+ getReq.onsuccess = function () {
+ const cursor = getReq.result;
+ if (cursor) {
+ rooms[cursor.key] = cursor.value;
+ cursor.continue();
+ } else {
+ try {
+ func(rooms);
+ } catch (e) {
+ abortWithException(txn, e);
+ }
+ }
+ };
+ }
+
+ // session backups
+
+ getSessionsNeedingBackup(limit) {
+ return new Promise((resolve, reject) => {
+ const sessions = [];
+ const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
+ txn.onerror = reject;
+ txn.oncomplete = function () {
+ resolve(sessions);
+ };
+ const objectStore = txn.objectStore("sessions_needing_backup");
+ const sessionStore = txn.objectStore("inbound_group_sessions");
+ const getReq = objectStore.openCursor();
+ getReq.onsuccess = function () {
+ const cursor = getReq.result;
+ if (cursor) {
+ const sessionGetReq = sessionStore.get(cursor.key);
+ sessionGetReq.onsuccess = function () {
+ sessions.push({
+ senderKey: sessionGetReq.result.senderCurve25519Key,
+ sessionId: sessionGetReq.result.sessionId,
+ sessionData: sessionGetReq.result.session
+ });
+ };
+ if (!limit || sessions.length < limit) {
+ cursor.continue();
+ }
+ }
+ };
+ });
+ }
+ countSessionsNeedingBackup(txn) {
+ 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 = () => resolve(req.result);
+ });
+ }
+ async unmarkSessionsNeedingBackup(sessions, txn) {
+ 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;
+ });
+ }));
+ }
+ async markSessionsNeedingBackup(sessions, txn) {
+ 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;
+ });
+ }));
+ }
+ addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
+ 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 = () => {
+ const {
+ sessions
+ } = req.result || {
+ sessions: []
+ };
+ sessions.push([senderKey, sessionId]);
+ objectStore.put({
+ roomId,
+ sessions
+ });
+ };
+ }
+ getSharedHistoryInboundGroupSessions(roomId, txn) {
+ 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 = () => {
+ const {
+ sessions
+ } = req.result || {
+ sessions: []
+ };
+ resolve(sessions);
+ };
+ req.onerror = reject;
+ });
+ }
+ addParkedSharedHistory(roomId, parkedData, txn) {
+ if (!txn) {
+ txn = this.db.transaction("parked_shared_history", "readwrite");
+ }
+ const objectStore = txn.objectStore("parked_shared_history");
+ const req = objectStore.get([roomId]);
+ req.onsuccess = () => {
+ const {
+ parked
+ } = req.result || {
+ parked: []
+ };
+ parked.push(parkedData);
+ objectStore.put({
+ roomId,
+ parked
+ });
+ };
+ }
+ takeParkedSharedHistory(roomId, txn) {
+ 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 = () => {
+ const cursor = cursorReq.result;
+ if (!cursor) {
+ resolve([]);
+ return;
+ }
+ const data = cursor.value;
+ cursor.delete();
+ resolve(data);
+ };
+ cursorReq.onerror = reject;
+ });
+ }
+ doTxn(mode, stores, func, log = _logger.logger) {
+ let startTime;
+ let description;
+ 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;
+ });
+ }
+}
+exports.Backend = Backend;
+const DB_MIGRATIONS = [db => {
+ createDatabase(db);
+}, db => {
+ db.createObjectStore("account");
+}, db => {
+ const sessionsStore = db.createObjectStore("sessions", {
+ keyPath: ["deviceKey", "sessionId"]
+ });
+ sessionsStore.createIndex("deviceKey", "deviceKey");
+}, db => {
+ db.createObjectStore("inbound_group_sessions", {
+ keyPath: ["senderCurve25519Key", "sessionId"]
+ });
+}, db => {
+ db.createObjectStore("device_data");
+}, db => {
+ db.createObjectStore("rooms");
+}, db => {
+ db.createObjectStore("sessions_needing_backup", {
+ keyPath: ["senderCurve25519Key", "sessionId"]
+ });
+}, db => {
+ db.createObjectStore("inbound_group_sessions_withheld", {
+ keyPath: ["senderCurve25519Key", "sessionId"]
+ });
+}, db => {
+ const problemsStore = db.createObjectStore("session_problems", {
+ keyPath: ["deviceKey", "time"]
+ });
+ problemsStore.createIndex("deviceKey", "deviceKey");
+ db.createObjectStore("notified_error_devices", {
+ keyPath: ["userId", "deviceId"]
+ });
+}, db => {
+ db.createObjectStore("shared_history_inbound_group_sessions", {
+ keyPath: ["roomId"]
+ });
+}, db => {
+ db.createObjectStore("parked_shared_history", {
+ keyPath: ["roomId"]
+ });
+}
+// Expand as needed.
+];
+
+const VERSION = DB_MIGRATIONS.length;
+exports.VERSION = VERSION;
+function upgradeDatabase(db, oldVersion) {
+ _logger.logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`);
+ DB_MIGRATIONS.forEach((migration, index) => {
+ if (oldVersion <= index) migration(db);
+ });
+}
+function createDatabase(db) {
+ 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");
+}
+/*
+ * Aborts a transaction with a given exception
+ * The transaction promise will be rejected with this exception.
+ */
+function abortWithException(txn, e) {
+ // 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._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(txn) {
+ return new Promise((resolve, reject) => {
+ txn.oncomplete = () => {
+ if (txn._mx_abortexception !== undefined) {
+ reject(txn._mx_abortexception);
+ }
+ resolve(null);
+ };
+ txn.onerror = event => {
+ if (txn._mx_abortexception !== undefined) {
+ reject(txn._mx_abortexception);
+ } else {
+ _logger.logger.log("Error performing indexeddb txn", event);
+ reject(txn.error);
+ }
+ };
+ txn.onabort = event => {
+ if (txn._mx_abortexception !== undefined) {
+ reject(txn._mx_abortexception);
+ } else {
+ _logger.logger.log("Error performing indexeddb txn", event);
+ reject(txn.error);
+ }
+ };
+ });
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js
new file mode 100644
index 0000000000..dc48bd400f
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js
@@ -0,0 +1,599 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.IndexedDBCryptoStore = void 0;
+var _logger = require("../../logger");
+var _localStorageCryptoStore = require("./localStorage-crypto-store");
+var _memoryCryptoStore = require("./memory-crypto-store");
+var IndexedDBCryptoStoreBackend = _interopRequireWildcard(require("./indexeddb-crypto-store-backend"));
+var _errors = require("../../errors");
+var IndexedDBHelpers = _interopRequireWildcard(require("../../indexeddb-helpers"));
+function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
+function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * Internal module. indexeddb storage for e2e.
+ */
+
+/**
+ * An implementation of CryptoStore, which is normally backed by an indexeddb,
+ * but with fallback to MemoryCryptoStore.
+ */
+class IndexedDBCryptoStore {
+ static exists(indexedDB, dbName) {
+ return IndexedDBHelpers.exists(indexedDB, dbName);
+ }
+ /**
+ * Create a new IndexedDBCryptoStore
+ *
+ * @param indexedDB - global indexedDB instance
+ * @param dbName - name of db to connect to
+ */
+ constructor(indexedDB, dbName) {
+ this.indexedDB = indexedDB;
+ this.dbName = dbName;
+ _defineProperty(this, "backendPromise", void 0);
+ _defineProperty(this, "backend", void 0);
+ }
+
+ /**
+ * 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
+ */
+ startup() {
+ if (this.backendPromise) {
+ return this.backendPromise;
+ }
+ this.backendPromise = new Promise((resolve, reject) => {
+ if (!this.indexedDB) {
+ reject(new Error("no indexeddb support available"));
+ return;
+ }
+ _logger.logger.log(`connecting to indexeddb ${this.dbName}`);
+ const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
+ req.onupgradeneeded = ev => {
+ const db = req.result;
+ const oldVersion = ev.oldVersion;
+ IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
+ };
+ req.onblocked = () => {
+ _logger.logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`);
+ };
+ req.onerror = ev => {
+ _logger.logger.log("Error connecting to indexeddb", ev);
+ reject(req.error);
+ };
+ req.onsuccess = () => {
+ const db = req.result;
+ _logger.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.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 _errors.InvalidCryptoStoreError(_errors.InvalidCryptoStoreState.TooNew);
+ }
+ _logger.logger.warn(`unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`);
+ try {
+ return new _localStorageCryptoStore.LocalStorageCryptoStore(global.localStorage);
+ } catch (e) {
+ _logger.logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
+ return new _memoryCryptoStore.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.
+ */
+ deleteAllData() {
+ return new Promise((resolve, reject) => {
+ if (!this.indexedDB) {
+ reject(new Error("no indexeddb support available"));
+ return;
+ }
+ _logger.logger.log(`Removing indexeddb instance: ${this.dbName}`);
+ const req = this.indexedDB.deleteDatabase(this.dbName);
+ req.onblocked = () => {
+ _logger.logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`);
+ };
+ req.onerror = ev => {
+ _logger.logger.log("Error deleting data from indexeddb", ev);
+ reject(req.error);
+ };
+ req.onsuccess = () => {
+ _logger.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.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.
+ */
+ getOrAddOutgoingRoomKeyRequest(request) {
+ 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
+ */
+ getOutgoingRoomKeyRequest(requestBody) {
+ 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.
+ */
+ getOutgoingRoomKeyRequestByState(wantedStates) {
+ 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
+ */
+ getAllOutgoingRoomKeyRequestsByState(wantedState) {
+ 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}
+ */
+ getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
+ 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
+ */
+ updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
+ 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
+ */
+ deleteOutgoingRoomKeyRequest(requestId, expectedState) {
+ 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
+ */
+ getAccount(txn, func) {
+ 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.
+ */
+ storeAccount(txn, accountPickle) {
+ 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
+ */
+ getCrossSigningKeys(txn, func) {
+ this.backend.getCrossSigningKeys(txn, func);
+ }
+
+ /**
+ * @param txn - An active transaction. See doTxn().
+ * @param func - Called with the private key
+ * @param type - A key type
+ */
+ getSecretStorePrivateKey(txn, func, type) {
+ 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()
+ */
+ storeCrossSigningKeys(txn, keys) {
+ 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()
+ */
+ storeSecretStorePrivateKey(txn, type, key) {
+ 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
+ */
+ countEndToEndSessions(txn, func) {
+ 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.
+ */
+ getEndToEndSession(deviceKey, sessionId, txn, func) {
+ 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.
+ */
+ getEndToEndSessions(deviceKey, txn, func) {
+ 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.
+ */
+ getAllEndToEndSessions(txn, func) {
+ 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().
+ */
+ storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+ this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
+ }
+ storeEndToEndSessionProblem(deviceKey, type, fixed) {
+ return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
+ }
+ getEndToEndSessionProblem(deviceKey, timestamp) {
+ return this.backend.getEndToEndSessionProblem(deviceKey, timestamp);
+ }
+ filterOutNotifiedErrorDevices(devices) {
+ 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.
+ */
+ getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
+ 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.
+ */
+ getAllEndToEndInboundGroupSessions(txn, func) {
+ 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().
+ */
+ addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ 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().
+ */
+ storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
+ }
+ storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) {
+ 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().
+ */
+ storeEndToEndDeviceData(deviceData, txn) {
+ 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
+ */
+ getEndToEndDeviceData(txn, func) {
+ 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().
+ */
+ storeEndToEndRoom(roomId, roomInfo, txn) {
+ 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
+ */
+ getEndToEndRooms(txn, func) {
+ 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
+ */
+ getSessionsNeedingBackup(limit) {
+ 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
+ */
+ countSessionsNeedingBackup(txn) {
+ 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
+ */
+ unmarkSessionsNeedingBackup(sessions, txn) {
+ 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
+ */
+ markSessionsNeedingBackup(sessions, txn) {
+ 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)
+ */
+ addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
+ 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]
+ */
+ getSharedHistoryInboundGroupSessions(roomId, txn) {
+ return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
+ }
+
+ /**
+ * Park a shared-history group session for a room we may be invited to later.
+ */
+ addParkedSharedHistory(roomId, parkedData, txn) {
+ this.backend.addParkedSharedHistory(roomId, parkedData, txn);
+ }
+
+ /**
+ * Pop out all shared-history group sessions for a room.
+ */
+ takeParkedSharedHistory(roomId, txn) {
+ 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.
+ */
+ doTxn(mode, stores, func, log) {
+ return this.backend.doTxn(mode, stores, func, log);
+ }
+}
+exports.IndexedDBCryptoStore = IndexedDBCryptoStore;
+_defineProperty(IndexedDBCryptoStore, "STORE_ACCOUNT", "account");
+_defineProperty(IndexedDBCryptoStore, "STORE_SESSIONS", "sessions");
+_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS", "inbound_group_sessions");
+_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS_WITHHELD", "inbound_group_sessions_withheld");
+_defineProperty(IndexedDBCryptoStore, "STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS", "shared_history_inbound_group_sessions");
+_defineProperty(IndexedDBCryptoStore, "STORE_PARKED_SHARED_HISTORY", "parked_shared_history");
+_defineProperty(IndexedDBCryptoStore, "STORE_DEVICE_DATA", "device_data");
+_defineProperty(IndexedDBCryptoStore, "STORE_ROOMS", "rooms");
+_defineProperty(IndexedDBCryptoStore, "STORE_BACKUP", "sessions_needing_backup"); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js
new file mode 100644
index 0000000000..17348d1813
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js
@@ -0,0 +1,329 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.LocalStorageCryptoStore = void 0;
+var _logger = require("../../logger");
+var _memoryCryptoStore = require("./memory-crypto-store");
+var _utils = require("../../utils");
+/*
+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.
+*/
+
+/**
+ * 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) {
+ return E2E_PREFIX + "sessions/" + deviceKey;
+}
+function keyEndToEndSessionProblems(deviceKey) {
+ return E2E_PREFIX + "session.problems/" + deviceKey;
+}
+function keyEndToEndInboundGroupSession(senderKey, sessionId) {
+ return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
+}
+function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
+ return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
+}
+function keyEndToEndRoomsPrefix(roomId) {
+ return KEY_ROOMS_PREFIX + roomId;
+}
+class LocalStorageCryptoStore extends _memoryCryptoStore.MemoryCryptoStore {
+ static exists(store) {
+ const length = store.length;
+ for (let i = 0; i < length; i++) {
+ if (store.key(i)?.startsWith(E2E_PREFIX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ constructor(store) {
+ super();
+ this.store = store;
+ }
+
+ // Olm Sessions
+
+ countEndToEndSessions(txn, func) {
+ 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
+ _getEndToEndSessions(deviceKey) {
+ const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
+ const fixedSessions = {};
+
+ // 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;
+ }
+ getEndToEndSession(deviceKey, sessionId, txn, func) {
+ const sessions = this._getEndToEndSessions(deviceKey);
+ func(sessions[sessionId] || {});
+ }
+ getEndToEndSessions(deviceKey, txn, func) {
+ func(this._getEndToEndSessions(deviceKey) || {});
+ }
+ getAllEndToEndSessions(txn, func) {
+ 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);
+ }
+ }
+ }
+ }
+ storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+ const sessions = this._getEndToEndSessions(deviceKey) || {};
+ sessions[sessionId] = sessionInfo;
+ setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
+ }
+ async storeEndToEndSessionProblem(deviceKey, type, fixed) {
+ const key = keyEndToEndSessionProblems(deviceKey);
+ const problems = getJsonItem(this.store, key) || [];
+ problems.push({
+ type,
+ fixed,
+ time: Date.now()
+ });
+ problems.sort((a, b) => {
+ return a.time - b.time;
+ });
+ setJsonItem(this.store, key, problems);
+ }
+ async getEndToEndSessionProblem(deviceKey, timestamp) {
+ const key = keyEndToEndSessionProblems(deviceKey);
+ const problems = getJsonItem(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;
+ }
+ }
+ async filterOutNotifiedErrorDevices(devices) {
+ const notifiedErrorDevices = getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
+ const ret = [];
+ for (const device of devices) {
+ const {
+ userId,
+ deviceInfo
+ } = device;
+ if (userId in notifiedErrorDevices) {
+ if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
+ ret.push(device);
+ (0, _utils.safeSet)(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
+ }
+ } else {
+ ret.push(device);
+ (0, _utils.safeSet)(notifiedErrorDevices, userId, {
+ [deviceInfo.deviceId]: true
+ });
+ }
+ }
+ setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
+ return ret;
+ }
+
+ // Inbound Group Sessions
+
+ getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
+ func(getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)), getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId)));
+ }
+ getAllEndToEndInboundGroupSessions(txn, func) {
+ 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);
+ }
+ addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId));
+ if (!existing) {
+ this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
+ }
+ }
+ storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData);
+ }
+ storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) {
+ setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData);
+ }
+ getEndToEndDeviceData(txn, func) {
+ func(getJsonItem(this.store, KEY_DEVICE_DATA));
+ }
+ storeEndToEndDeviceData(deviceData, txn) {
+ setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
+ }
+ storeEndToEndRoom(roomId, roomInfo, txn) {
+ setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
+ }
+ getEndToEndRooms(txn, func) {
+ const result = {};
+ 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);
+ }
+ getSessionsNeedingBackup(limit) {
+ const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
+ const sessions = [];
+ 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);
+ }
+ countSessionsNeedingBackup() {
+ const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
+ return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
+ }
+ unmarkSessionsNeedingBackup(sessions) {
+ const sessionsNeedingBackup = getJsonItem(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();
+ }
+ markSessionsNeedingBackup(sessions) {
+ const sessionsNeedingBackup = getJsonItem(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.
+ */
+ deleteAllData() {
+ this.store.removeItem(KEY_END_TO_END_ACCOUNT);
+ return Promise.resolve();
+ }
+
+ // Olm account
+
+ getAccount(txn, func) {
+ const accountPickle = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT);
+ func(accountPickle);
+ }
+ storeAccount(txn, accountPickle) {
+ setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
+ }
+ getCrossSigningKeys(txn, func) {
+ const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS);
+ func(keys);
+ }
+ getSecretStorePrivateKey(txn, func, type) {
+ const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
+ func(key);
+ }
+ storeCrossSigningKeys(txn, keys) {
+ setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
+ }
+ storeSecretStorePrivateKey(txn, type, key) {
+ setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
+ }
+ doTxn(mode, stores, func) {
+ return Promise.resolve(func(null));
+ }
+}
+exports.LocalStorageCryptoStore = LocalStorageCryptoStore;
+function getJsonItem(store, key) {
+ 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.logger.log("Error: Failed to get key %s: %s", key, e.message);
+ _logger.logger.log(e.stack);
+ }
+ return null;
+}
+function setJsonItem(store, key, val) {
+ store.setItem(key, JSON.stringify(val));
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js
new file mode 100644
index 0000000000..5b9fba0289
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js
@@ -0,0 +1,439 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MemoryCryptoStore = void 0;
+var _logger = require("../../logger");
+var _utils = require("../../utils");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * Internal module. in-memory storage for e2e.
+ */
+
+class MemoryCryptoStore {
+ constructor() {
+ _defineProperty(this, "outgoingRoomKeyRequests", []);
+ _defineProperty(this, "account", null);
+ _defineProperty(this, "crossSigningKeys", null);
+ _defineProperty(this, "privateKeys", {});
+ _defineProperty(this, "sessions", {});
+ _defineProperty(this, "sessionProblems", {});
+ _defineProperty(this, "notifiedErrorDevices", {});
+ _defineProperty(this, "inboundGroupSessions", {});
+ _defineProperty(this, "inboundGroupSessionsWithheld", {});
+ // Opaque device data object
+ _defineProperty(this, "deviceData", null);
+ _defineProperty(this, "rooms", {});
+ _defineProperty(this, "sessionsNeedingBackup", {});
+ _defineProperty(this, "sharedHistoryInboundGroupSessions", {});
+ _defineProperty(this, "parkedSharedHistory", new Map());
+ }
+ // 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.
+ */
+ async startup() {
+ // 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.
+ */
+ deleteAllData() {
+ 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.
+ */
+ getOrAddOutgoingRoomKeyRequest(request) {
+ const requestBody = request.requestBody;
+ return (0, _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.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.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
+ */
+ getOutgoingRoomKeyRequest(requestBody) {
+ 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
+ _getOutgoingRoomKeyRequest(requestBody) {
+ for (const existing of this.outgoingRoomKeyRequests) {
+ if ((0, _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
+ */
+ getOutgoingRoomKeyRequestByState(wantedStates) {
+ 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
+ */
+ getAllOutgoingRoomKeyRequestsByState(wantedState) {
+ return Promise.resolve(this.outgoingRoomKeyRequests.filter(r => r.state == wantedState));
+ }
+ getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
+ const results = [];
+ 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
+ */
+ updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
+ for (const req of this.outgoingRoomKeyRequests) {
+ if (req.requestId !== requestId) {
+ continue;
+ }
+ if (req.state !== expectedState) {
+ _logger.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
+ */
+ deleteOutgoingRoomKeyRequest(requestId, expectedState) {
+ for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
+ const req = this.outgoingRoomKeyRequests[i];
+ if (req.requestId !== requestId) {
+ continue;
+ }
+ if (req.state != expectedState) {
+ _logger.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
+
+ getAccount(txn, func) {
+ func(this.account);
+ }
+ storeAccount(txn, accountPickle) {
+ this.account = accountPickle;
+ }
+ getCrossSigningKeys(txn, func) {
+ func(this.crossSigningKeys);
+ }
+ getSecretStorePrivateKey(txn, func, type) {
+ const result = this.privateKeys[type];
+ func(result || null);
+ }
+ storeCrossSigningKeys(txn, keys) {
+ this.crossSigningKeys = keys;
+ }
+ storeSecretStorePrivateKey(txn, type, key) {
+ this.privateKeys[type] = key;
+ }
+
+ // Olm Sessions
+
+ countEndToEndSessions(txn, func) {
+ func(Object.keys(this.sessions).length);
+ }
+ getEndToEndSession(deviceKey, sessionId, txn, func) {
+ const deviceSessions = this.sessions[deviceKey] || {};
+ func(deviceSessions[sessionId] || null);
+ }
+ getEndToEndSessions(deviceKey, txn, func) {
+ func(this.sessions[deviceKey] || {});
+ }
+ getAllEndToEndSessions(txn, func) {
+ Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
+ Object.entries(deviceSessions).forEach(([sessionId, session]) => {
+ func(_objectSpread(_objectSpread({}, session), {}, {
+ deviceKey,
+ sessionId
+ }));
+ });
+ });
+ }
+ storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+ let deviceSessions = this.sessions[deviceKey];
+ if (deviceSessions === undefined) {
+ deviceSessions = {};
+ this.sessions[deviceKey] = deviceSessions;
+ }
+ (0, _utils.safeSet)(deviceSessions, sessionId, sessionInfo);
+ }
+ async storeEndToEndSessionProblem(deviceKey, type, fixed) {
+ const problems = this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || [];
+ problems.push({
+ type,
+ fixed,
+ time: Date.now()
+ });
+ problems.sort((a, b) => {
+ return a.time - b.time;
+ });
+ }
+ async getEndToEndSessionProblem(deviceKey, timestamp) {
+ 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;
+ }
+ }
+ async filterOutNotifiedErrorDevices(devices) {
+ const notifiedErrorDevices = this.notifiedErrorDevices;
+ const ret = [];
+ for (const device of devices) {
+ const {
+ userId,
+ deviceInfo
+ } = device;
+ if (userId in notifiedErrorDevices) {
+ if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
+ ret.push(device);
+ (0, _utils.safeSet)(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
+ }
+ } else {
+ ret.push(device);
+ (0, _utils.safeSet)(notifiedErrorDevices, userId, {
+ [deviceInfo.deviceId]: true
+ });
+ }
+ }
+ return ret;
+ }
+
+ // Inbound Group Sessions
+
+ getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
+ const k = senderCurve25519Key + "/" + sessionId;
+ func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
+ }
+ getAllEndToEndInboundGroupSessions(txn, func) {
+ 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);
+ }
+ addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ const k = senderCurve25519Key + "/" + sessionId;
+ if (this.inboundGroupSessions[k] === undefined) {
+ this.inboundGroupSessions[k] = sessionData;
+ }
+ }
+ storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
+ this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
+ }
+ storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) {
+ const k = senderCurve25519Key + "/" + sessionId;
+ this.inboundGroupSessionsWithheld[k] = sessionData;
+ }
+
+ // Device Data
+
+ getEndToEndDeviceData(txn, func) {
+ func(this.deviceData);
+ }
+ storeEndToEndDeviceData(deviceData, txn) {
+ this.deviceData = deviceData;
+ }
+
+ // E2E rooms
+
+ storeEndToEndRoom(roomId, roomInfo, txn) {
+ this.rooms[roomId] = roomInfo;
+ }
+ getEndToEndRooms(txn, func) {
+ func(this.rooms);
+ }
+ getSessionsNeedingBackup(limit) {
+ const sessions = [];
+ 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);
+ }
+ countSessionsNeedingBackup() {
+ return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
+ }
+ unmarkSessionsNeedingBackup(sessions) {
+ for (const session of sessions) {
+ const sessionKey = session.senderKey + "/" + session.sessionId;
+ delete this.sessionsNeedingBackup[sessionKey];
+ }
+ return Promise.resolve();
+ }
+ markSessionsNeedingBackup(sessions) {
+ for (const session of sessions) {
+ const sessionKey = session.senderKey + "/" + session.sessionId;
+ this.sessionsNeedingBackup[sessionKey] = true;
+ }
+ return Promise.resolve();
+ }
+ addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
+ const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
+ sessions.push([senderKey, sessionId]);
+ this.sharedHistoryInboundGroupSessions[roomId] = sessions;
+ }
+ getSharedHistoryInboundGroupSessions(roomId) {
+ return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
+ }
+ addParkedSharedHistory(roomId, parkedData) {
+ const parked = this.parkedSharedHistory.get(roomId) ?? [];
+ parked.push(parkedData);
+ this.parkedSharedHistory.set(roomId, parked);
+ }
+ takeParkedSharedHistory(roomId) {
+ const parked = this.parkedSharedHistory.get(roomId) ?? [];
+ this.parkedSharedHistory.delete(roomId);
+ return Promise.resolve(parked);
+ }
+
+ // Session key backups
+
+ doTxn(mode, stores, func) {
+ return Promise.resolve(func(null));
+ }
+}
+exports.MemoryCryptoStore = MemoryCryptoStore; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js
new file mode 100644
index 0000000000..4da45f880e
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js
@@ -0,0 +1,345 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.VerificationEvent = exports.VerificationBase = exports.SwitchStartEventError = void 0;
+var _event = require("../../models/event");
+var _event2 = require("../../@types/event");
+var _logger = require("../../logger");
+var _deviceinfo = require("../deviceinfo");
+var _Error = require("./Error");
+var _CrossSigning = require("../CrossSigning");
+var _typedEventEmitter = require("../../models/typed-event-emitter");
+var _verification = require("../../crypto-api/verification");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const timeoutException = new Error("Verification timed out");
+class SwitchStartEventError extends Error {
+ constructor(startEvent) {
+ super();
+ this.startEvent = startEvent;
+ }
+}
+
+/** @deprecated use VerifierEvent */
+exports.SwitchStartEventError = SwitchStartEventError;
+/** @deprecated use VerifierEvent */
+const VerificationEvent = _verification.VerifierEvent;
+
+/** @deprecated use VerifierEventHandlerMap */
+exports.VerificationEvent = VerificationEvent;
+// The type parameters of VerificationBase are no longer used, but we need some placeholders to maintain
+// backwards compatibility with applications that reference the class.
+class VerificationBase extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * 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
+ */
+ constructor(channel, baseApis, userId, deviceId, startEvent, request) {
+ super();
+ this.channel = channel;
+ this.baseApis = baseApis;
+ this.userId = userId;
+ this.deviceId = deviceId;
+ this.startEvent = startEvent;
+ this.request = request;
+ _defineProperty(this, "cancelled", false);
+ _defineProperty(this, "_done", false);
+ _defineProperty(this, "promise", null);
+ _defineProperty(this, "transactionTimeoutTimer", null);
+ _defineProperty(this, "expectedEvent", void 0);
+ _defineProperty(this, "resolve", void 0);
+ _defineProperty(this, "reject", void 0);
+ _defineProperty(this, "resolveEvent", void 0);
+ _defineProperty(this, "rejectEvent", void 0);
+ _defineProperty(this, "started", void 0);
+ _defineProperty(this, "doVerification", void 0);
+ }
+ get initiatedByMe() {
+ // 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();
+ }
+ get hasBeenCancelled() {
+ return this.cancelled;
+ }
+ resetTimer() {
+ _logger.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.logger.info("Triggering verification timeout");
+ this.cancel(timeoutException);
+ }
+ }, 10 * 60 * 1000); // 10 minutes
+ }
+
+ endTimer() {
+ if (this.transactionTimeoutTimer !== null) {
+ clearTimeout(this.transactionTimeoutTimer);
+ this.transactionTimeoutTimer = null;
+ }
+ }
+ send(type, uncompletedContent) {
+ return this.channel.send(type, uncompletedContent);
+ }
+ waitForEvent(type) {
+ 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;
+ });
+ }
+ canSwitchStartEvent(event) {
+ return false;
+ }
+ switchStartEvent(event) {
+ if (this.canSwitchStartEvent(event)) {
+ _logger.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;
+ }
+ }
+ }
+ handleEvent(e) {
+ 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 !== _event2.EventType.KeyVerificationDone) {
+ this.expectedEvent = undefined;
+ this.rejectEvent = undefined;
+ this.resetTimer();
+ this.resolveEvent?.(e);
+ }
+ } else if (e.getType() === _event2.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);
+ }
+ }
+ async done() {
+ this.endTimer(); // always kill the activity timer
+ if (!this._done) {
+ this.request.onVerifierFinished();
+ this.resolve?.();
+ return (0, _CrossSigning.requestKeysDuringVerification)(this.baseApis, this.userId, this.deviceId);
+ }
+ }
+ cancel(e) {
+ 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 = (0, _Error.newTimeoutError)();
+ this.send(timeoutEvent.getType(), timeoutEvent.getContent());
+ } else if (e instanceof _event.MatrixEvent) {
+ const sender = e.getSender();
+ if (sender !== this.userId) {
+ const content = e.getContent();
+ if (e.getType() === _event2.EventType.KeyVerificationCancel) {
+ content.code = content.code || "m.unknown";
+ content.reason = content.reason || content.body || "Unknown reason";
+ this.send(_event2.EventType.KeyVerificationCancel, content);
+ } else {
+ this.send(_event2.EventType.KeyVerificationCancel, {
+ code: "m.unknown",
+ reason: content.body || "Unknown reason"
+ });
+ }
+ }
+ } else {
+ this.send(_event2.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.
+ */
+ verify() {
+ if (this.promise) return this.promise;
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = (...args) => {
+ this._done = true;
+ this.endTimer();
+ resolve(...args);
+ };
+ this.reject = e => {
+ this._done = true;
+ this.endTimer();
+ reject(e);
+ };
+ });
+ if (this.doVerification && !this.started) {
+ this.started = true;
+ this.resetTimer(); // restart the timeout
+ new Promise((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;
+ }
+ async verifyKeys(userId, keys, verifier) {
+ // 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 = [];
+ 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.DeviceInfo.fromStorage({
+ keys: {
+ [keyId]: deviceId
+ }
+ }, deviceId), keyInfo);
+ verifiedDevices.push([deviceId, keyId, deviceId]);
+ } else {
+ _logger.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.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();
+ }
+ }
+ get events() {
+ return undefined;
+ }
+}
+exports.VerificationBase = VerificationBase; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js
new file mode 100644
index 0000000000..3d24c03955
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js
@@ -0,0 +1,100 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.errorFactory = errorFactory;
+exports.errorFromEvent = errorFromEvent;
+exports.newUserCancelledError = exports.newUnknownMethodError = exports.newUnexpectedMessageError = exports.newTimeoutError = exports.newKeyMismatchError = exports.newInvalidMessageError = void 0;
+exports.newVerificationError = newVerificationError;
+var _event = require("../../models/event");
+var _event2 = require("../../@types/event");
+/*
+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.
+ */
+
+function newVerificationError(code, reason, extraData) {
+ const content = Object.assign({}, {
+ code,
+ reason
+ }, extraData);
+ return new _event.MatrixEvent({
+ type: _event2.EventType.KeyVerificationCancel,
+ content
+ });
+}
+function errorFactory(code, reason) {
+ return function (extraData) {
+ return newVerificationError(code, reason, extraData);
+ };
+}
+
+/**
+ * The verification was cancelled by the user.
+ */
+const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
+
+/**
+ * The verification timed out.
+ */
+exports.newUserCancelledError = newUserCancelledError;
+const newTimeoutError = errorFactory("m.timeout", "Timed out");
+
+/**
+ * An unknown method was selected.
+ */
+exports.newTimeoutError = newTimeoutError;
+const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
+
+/**
+ * An unexpected message was sent.
+ */
+exports.newUnknownMethodError = newUnknownMethodError;
+const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message");
+
+/**
+ * The key does not match.
+ */
+exports.newUnexpectedMessageError = newUnexpectedMessageError;
+const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch");
+
+/**
+ * An invalid message was sent.
+ */
+exports.newKeyMismatchError = newKeyMismatchError;
+const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message");
+exports.newInvalidMessageError = newInvalidMessageError;
+function errorFromEvent(event) {
+ const content = event.getContent();
+ if (content) {
+ const {
+ code,
+ reason
+ } = content;
+ return {
+ code,
+ reason
+ };
+ } else {
+ return {
+ code: "Unknown error",
+ reason: "m.unknown"
+ };
+ }
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js
new file mode 100644
index 0000000000..396d911eec
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js
@@ -0,0 +1,46 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.IllegalMethod = void 0;
+var _Base = require("./Base");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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).
+ */
+class IllegalMethod extends _Base.VerificationBase {
+ constructor(...args) {
+ super(...args);
+ _defineProperty(this, "doVerification", async () => {
+ throw new Error("Verification is not possible with this method");
+ });
+ }
+ static factory(channel, baseApis, userId, deviceId, startEvent, request) {
+ return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ static get NAME() {
+ // Typically the name will be something else, but to complete
+ // the contract we offer a default one here.
+ return "org.matrix.illegal_method";
+ }
+}
+exports.IllegalMethod = IllegalMethod; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js
new file mode 100644
index 0000000000..e2334c64e7
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js
@@ -0,0 +1,269 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SHOW_QR_CODE_METHOD = exports.SCAN_QR_CODE_METHOD = exports.ReciprocateQRCode = exports.QrCodeEvent = exports.QRCodeData = void 0;
+var _Base = require("./Base");
+var _Error = require("./Error");
+var _olmlib = require("../olmlib");
+var _logger = require("../../logger");
+var _verification = require("../../crypto-api/verification");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
+exports.SHOW_QR_CODE_METHOD = SHOW_QR_CODE_METHOD;
+const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
+
+/** @deprecated use VerifierEvent */
+exports.SCAN_QR_CODE_METHOD = SCAN_QR_CODE_METHOD;
+/** @deprecated use VerifierEvent */
+const QrCodeEvent = _verification.VerifierEvent;
+exports.QrCodeEvent = QrCodeEvent;
+class ReciprocateQRCode extends _Base.VerificationBase {
+ constructor(...args) {
+ super(...args);
+ _defineProperty(this, "reciprocateQREvent", void 0);
+ _defineProperty(this, "doVerification", async () => {
+ 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 (0, _Error.newKeyMismatchError)();
+ }
+
+ // 2. ask if other user shows shield as well
+ await new Promise((resolve, reject) => {
+ this.reciprocateQREvent = {
+ confirm: resolve,
+ cancel: () => reject((0, _Error.newUserCancelledError)())
+ };
+ this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
+ });
+
+ // 3. determine key to sign / mark as trusted
+ const keys = {};
+ 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 (0, _Error.newKeyMismatchError)();
+ if (keyInfo !== targetKey) {
+ _logger.logger.error("key ID from key info does not match");
+ throw (0, _Error.newKeyMismatchError)();
+ }
+ for (const deviceKeyId in device.keys) {
+ if (!deviceKeyId.startsWith("ed25519")) continue;
+ const deviceTargetKey = keys[deviceKeyId];
+ if (!deviceTargetKey) throw (0, _Error.newKeyMismatchError)();
+ if (device.keys[deviceKeyId] !== deviceTargetKey) {
+ _logger.logger.error("master key does not match");
+ throw (0, _Error.newKeyMismatchError)();
+ }
+ }
+ });
+ });
+ }
+ static factory(channel, baseApis, userId, deviceId, startEvent, request) {
+ return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ static get NAME() {
+ return "m.reciprocate.v1";
+ }
+}
+exports.ReciprocateQRCode = ReciprocateQRCode;
+const CODE_VERSION = 0x02; // the version of binary QR codes we support
+const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
+var Mode = /*#__PURE__*/function (Mode) {
+ Mode[Mode["VerifyOtherUser"] = 0] = "VerifyOtherUser";
+ Mode[Mode["VerifySelfTrusted"] = 1] = "VerifySelfTrusted";
+ Mode[Mode["VerifySelfUntrusted"] = 2] = "VerifySelfUntrusted";
+ return Mode;
+}(Mode || {}); // We do not trust the master key
+class QRCodeData {
+ constructor(mode, sharedSecret,
+ // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
+ otherUserMasterKey,
+ // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
+ otherDeviceKey,
+ // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
+ myMasterKey, buffer) {
+ this.mode = mode;
+ this.sharedSecret = sharedSecret;
+ this.otherUserMasterKey = otherUserMasterKey;
+ this.otherDeviceKey = otherDeviceKey;
+ this.myMasterKey = myMasterKey;
+ this.buffer = buffer;
+ }
+ static async create(request, client) {
+ const sharedSecret = QRCodeData.generateSharedSecret();
+ const mode = QRCodeData.determineMode(request, client);
+ let otherUserMasterKey = null;
+ let otherDeviceKey = null;
+ let myMasterKey = 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.
+ */
+ get encodedSharedSecret() {
+ return this.sharedSecret;
+ }
+ getBuffer() {
+ return this.buffer;
+ }
+ static generateSharedSecret() {
+ const secretBytes = new Uint8Array(11);
+ global.crypto.getRandomValues(secretBytes);
+ return (0, _olmlib.encodeUnpaddedBase64)(secretBytes);
+ }
+ static async getOtherDeviceKey(request, client) {
+ 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();
+ }
+ static determineMode(request, client) {
+ 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;
+ }
+ static generateQrData(request, client, mode, encodedSharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey) {
+ const myUserId = client.getUserId();
+ const transactionId = request.channel.transactionId;
+ const qrData = {
+ 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;
+ }
+ static generateBuffer(qrData) {
+ let buf = Buffer.alloc(0); // we'll concat our way through life
+
+ const appendByte = b => {
+ const tmpBuf = Buffer.from([b]);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendInt = i => {
+ const tmpBuf = Buffer.alloc(2);
+ tmpBuf.writeInt16BE(i, 0);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendStr = (s, enc, withLengthPrefix = true) => {
+ const tmpBuf = Buffer.from(s, enc);
+ if (withLengthPrefix) appendInt(tmpBuf.byteLength);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendEncBase64 = b64 => {
+ const b = (0, _olmlib.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;
+ }
+}
+exports.QRCodeData = QRCodeData; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js
new file mode 100644
index 0000000000..fac79f7a00
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js
@@ -0,0 +1,454 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SasEvent = exports.SAS = void 0;
+var _anotherJson = _interopRequireDefault(require("another-json"));
+var _Base = require("./Base");
+var _Error = require("./Error");
+var _logger = require("../../logger");
+var _SASDecimal = require("./SASDecimal");
+var _event = require("../../@types/event");
+var _verification = require("../../crypto-api/verification");
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+// backwards-compatibility exports
+
+const START_TYPE = _event.EventType.KeyVerificationStart;
+const EVENTS = [_event.EventType.KeyVerificationAccept, _event.EventType.KeyVerificationKey, _event.EventType.KeyVerificationMac];
+let olmutil;
+const newMismatchedSASError = (0, _Error.errorFactory)("m.mismatched_sas", "Mismatched short authentication string");
+const newMismatchedCommitmentError = (0, _Error.errorFactory)("m.mismatched_commitment", "Mismatched commitment");
+const 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) {
+ 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: _SASDecimal.generateDecimalSas,
+ emoji: generateEmojiSas
+};
+function generateSas(sasBytes, methods) {
+ const sas = {};
+ 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"
+};
+function calculateMAC(olmSAS, method) {
+ return function (input, info) {
+ const mac = olmSAS[macMethods[method]](input, info);
+ _logger.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, olmSAS, bytes) {
+ 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, olmSAS, bytes) {
+ 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);
+ }
+};
+/* 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 = ["curve25519-hkdf-sha256", "curve25519"];
+const HASHES_LIST = ["sha256"];
+const MAC_LIST = ["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(anArray, aSet) {
+ return Array.isArray(anArray) ? anArray.filter(x => aSet.has(x)) : [];
+}
+
+/** @deprecated use VerifierEvent */
+
+/** @deprecated use VerifierEvent */
+const SasEvent = _verification.VerifierEvent;
+exports.SasEvent = SasEvent;
+class SAS extends _Base.VerificationBase {
+ constructor(...args) {
+ super(...args);
+ _defineProperty(this, "waitingForAccept", void 0);
+ _defineProperty(this, "ourSASPubKey", void 0);
+ _defineProperty(this, "theirSASPubKey", void 0);
+ _defineProperty(this, "sasEvent", void 0);
+ _defineProperty(this, "doVerification", async () => {
+ 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 _Base.SwitchStartEventError) {
+ // this changes what initiatedByMe returns
+ this.startEvent = err.startEvent;
+ retry = true;
+ } else {
+ throw err;
+ }
+ }
+ } while (retry);
+ });
+ }
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ static get NAME() {
+ return "m.sas.v1";
+ }
+ get events() {
+ return EVENTS;
+ }
+ canSwitchStartEvent(event) {
+ if (event.getType() !== START_TYPE) {
+ return false;
+ }
+ const content = event.getContent();
+ return content?.method === SAS.NAME && !!this.waitingForAccept;
+ }
+ async sendStart() {
+ 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;
+ }
+ async verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod) {
+ const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
+ const verifySAS = new Promise((resolve, reject) => {
+ this.sasEvent = {
+ sas: generateSas(sasBytes, sasMethods),
+ confirm: async () => {
+ try {
+ await this.sendMAC(olmSAS, macMethod);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ },
+ cancel: () => reject((0, _Error.newUserCancelledError)()),
+ mismatch: () => reject(newMismatchedSASError())
+ };
+ this.emit(SasEvent.ShowSas, this.sasEvent);
+ });
+ const [e] = await Promise.all([this.waitForEvent(_event.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 = _event.EventType.KeyVerificationDone;
+ return e;
+ }), verifySAS]);
+ const content = e.getContent();
+ await this.checkMAC(olmSAS, content, macMethod);
+ }
+ async doSendVerification() {
+ 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 _Base.SwitchStartEventError(this.startEvent);
+ }
+ let e;
+ try {
+ e = await this.waitForEvent(_event.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 (0, _Error.newUnknownMethodError)();
+ }
+ if (typeof content.commitment !== "string") {
+ throw (0, _Error.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(_event.EventType.KeyVerificationKey, {
+ key: this.ourSASPubKey
+ });
+ e = await this.waitForEvent(_event.EventType.KeyVerificationKey);
+ // FIXME: make sure event is properly formed
+ content = e.getContent();
+ const commitmentStr = content.key + _anotherJson.default.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();
+ }
+ }
+ async doRespondVerification() {
+ // 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 (0, _Error.newUnknownMethodError)();
+ }
+ const olmSAS = new global.Olm.SAS();
+ try {
+ const commitmentStr = olmSAS.get_pubkey() + _anotherJson.default.stringify(content);
+ await this.send(_event.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(_event.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(_event.EventType.KeyVerificationKey, {
+ key: this.ourSASPubKey
+ });
+ await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
+ } finally {
+ olmSAS.free();
+ }
+ }
+ sendMAC(olmSAS, method) {
+ const mac = {};
+ const keyList = [];
+ 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(_event.EventType.KeyVerificationMac, {
+ mac,
+ keys
+ });
+ }
+ async checkMAC(olmSAS, content, method) {
+ 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 (0, _Error.newKeyMismatchError)();
+ }
+ await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
+ if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) {
+ throw (0, _Error.newKeyMismatchError)();
+ }
+ });
+ }
+}
+exports.SAS = SAS; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js
new file mode 100644
index 0000000000..7cd8fb2505
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js
@@ -0,0 +1,39 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.generateDecimalSas = generateDecimalSas;
+/*
+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
+ */
+function generateDecimalSas(sasBytes) {
+ /*
+ * +--------+--------+--------+--------+--------+
+ * | 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];
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/Channel.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/Channel.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/Channel.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js
new file mode 100644
index 0000000000..15c7fcae5a
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js
@@ -0,0 +1,349 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.InRoomRequests = exports.InRoomChannel = void 0;
+var _VerificationRequest = require("./VerificationRequest");
+var _logger = require("../../../logger");
+var _event = require("../../../@types/event");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const MESSAGE_TYPE = _event.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.
+ */
+class InRoomChannel {
+ /**
+ * @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.
+ */
+ constructor(client, roomId, userId) {
+ this.client = client;
+ this.roomId = roomId;
+ this.userId = userId;
+ _defineProperty(this, "requestEventId", void 0);
+ }
+ get receiveStartFromOtherDevices() {
+ return true;
+ }
+
+ /** The transaction id generated/used by this verification channel */
+ get transactionId() {
+ return this.requestEventId;
+ }
+ static getOtherPartyUserId(event, client) {
+ const type = InRoomChannel.getEventType(event);
+ if (type !== _VerificationRequest.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
+ */
+ getTimestamp(event) {
+ 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
+ */
+ static canCreateRequest(type) {
+ return type === _VerificationRequest.REQUEST_TYPE;
+ }
+ canCreateRequest(type) {
+ 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
+ */
+ static getTransactionId(event) {
+ if (InRoomChannel.getEventType(event) === _VerificationRequest.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
+ */
+ static validateEvent(event, client) {
+ 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 === _VerificationRequest.REQUEST_TYPE) {
+ if (!content || typeof content.to !== "string" || !content.to.length) {
+ _logger.logger.log("InRoomChannel: validateEvent: " + "no valid to " + content.to);
+ return false;
+ }
+
+ // ignore requests that are not direct to or sent by the syncing user
+ if (!InRoomChannel.getOtherPartyUserId(event, client)) {
+ _logger.logger.log("InRoomChannel: validateEvent: " + `not directed to or sent by me: ${event.getSender()}` + `, ${content.to}`);
+ return false;
+ }
+ }
+ return _VerificationRequest.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
+ */
+ static getEventType(event) {
+ const type = event.getType();
+ if (type === MESSAGE_TYPE) {
+ const content = event.getContent();
+ if (content) {
+ const {
+ msgtype
+ } = content;
+ if (msgtype === _VerificationRequest.REQUEST_TYPE) {
+ return _VerificationRequest.REQUEST_TYPE;
+ }
+ }
+ }
+ if (type && type !== _VerificationRequest.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.
+ */
+ async handleEvent(event, request, isLiveEvent = false) {
+ // 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.logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`);
+ return;
+ }
+ }
+ if (!this.requestEventId) {
+ this.requestEventId = InRoomChannel.getTransactionId(event);
+ }
+
+ // With pendingEventOrdering: "chronological", we will see events that have been sent but not yet reflected
+ // back via /sync. These are "local echoes" and are identifiable by their txnId
+ const isLocalEcho = !!event.getTxnId();
+
+ // Alternatively, we may see an event that we sent that is reflected back via /sync. These are "remote echoes"
+ // and have a transaction ID in the "unsigned" data
+ const isRemoteEcho = !!event.getUnsigned().transaction_id;
+ const isSentByUs = event.getSender() === this.client.getUserId();
+ return request.handleEvent(type, event, isLiveEvent, isLocalEcho || 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
+ */
+ completedContentFromEvent(event) {
+ // 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.
+ */
+ completeContent(type, content) {
+ content = Object.assign({}, content);
+ if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) {
+ content.from_device = this.client.getDeviceId();
+ }
+ if (type === _VerificationRequest.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: _VerificationRequest.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
+ */
+ send(type, uncompletedContent) {
+ 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
+ */
+ async sendCompleted(type, content) {
+ let sendType = type;
+ if (type === _VerificationRequest.REQUEST_TYPE) {
+ sendType = MESSAGE_TYPE;
+ }
+ const response = await this.client.sendEvent(this.roomId, sendType, content);
+ if (type === _VerificationRequest.REQUEST_TYPE) {
+ this.requestEventId = response.event_id;
+ }
+ }
+}
+exports.InRoomChannel = InRoomChannel;
+class InRoomRequests {
+ constructor() {
+ _defineProperty(this, "requestsByRoomId", new Map());
+ }
+ getRequest(event) {
+ const roomId = event.getRoomId();
+ const txnId = InRoomChannel.getTransactionId(event);
+ return this.getRequestByTxnId(roomId, txnId);
+ }
+ getRequestByChannel(channel) {
+ return this.getRequestByTxnId(channel.roomId, channel.transactionId);
+ }
+ getRequestByTxnId(roomId, txnId) {
+ const requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (requestsByTxnId) {
+ return requestsByTxnId.get(txnId);
+ }
+ }
+ setRequest(event, request) {
+ this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
+ }
+ setRequestByChannel(channel, request) {
+ this.doSetRequest(channel.roomId, channel.transactionId, request);
+ }
+ doSetRequest(roomId, txnId, request) {
+ let requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (!requestsByTxnId) {
+ requestsByTxnId = new Map();
+ this.requestsByRoomId.set(roomId, requestsByTxnId);
+ }
+ requestsByTxnId.set(txnId, request);
+ }
+ removeRequest(event) {
+ 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);
+ }
+ }
+ }
+ findRequestInProgress(roomId) {
+ const requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (requestsByTxnId) {
+ for (const request of requestsByTxnId.values()) {
+ if (request.pending) {
+ return request;
+ }
+ }
+ }
+ }
+}
+exports.InRoomRequests = InRoomRequests; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js
new file mode 100644
index 0000000000..781ec9358f
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js
@@ -0,0 +1,322 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ToDeviceRequests = exports.ToDeviceChannel = void 0;
+var _randomstring = require("../../../randomstring");
+var _logger = require("../../../logger");
+var _VerificationRequest = require("./VerificationRequest");
+var _Error = require("../Error");
+var _event = require("../../../models/event");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * A key verification channel that sends verification events over to_device messages.
+ * Generates its own transaction ids.
+ */
+class ToDeviceChannel {
+ // userId and devices of user we're about to verify
+ constructor(client, userId, devices, transactionId, deviceId) {
+ this.client = client;
+ this.userId = userId;
+ this.devices = devices;
+ this.transactionId = transactionId;
+ this.deviceId = deviceId;
+ _defineProperty(this, "request", void 0);
+ }
+ isToDevices(devices) {
+ if (devices.length === this.devices.length) {
+ for (const device of devices) {
+ if (!this.devices.includes(device)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ static getEventType(event) {
+ return event.getType();
+ }
+
+ /**
+ * Extract the transaction id used by a given key verification event, if any
+ * @param event - the event
+ * @returns the transaction id
+ */
+ static getTransactionId(event) {
+ 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
+ */
+ static canCreateRequest(type) {
+ return type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.START_TYPE;
+ }
+ canCreateRequest(type) {
+ 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
+ */
+ static validateEvent(event, client) {
+ if (event.isCancelled()) {
+ _logger.logger.warn("Ignoring flagged verification request from " + event.getSender());
+ return false;
+ }
+ const content = event.getContent();
+ if (!content) {
+ _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
+ return false;
+ }
+ if (!content.transaction_id) {
+ _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
+ return false;
+ }
+ const type = event.getType();
+ if (type === _VerificationRequest.REQUEST_TYPE) {
+ if (!Number.isFinite(content.timestamp)) {
+ _logger.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.logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
+ return false;
+ }
+ }
+ return _VerificationRequest.VerificationRequest.validateEvent(type, event, client);
+ }
+
+ /**
+ * @param event - the event to get the timestamp of
+ * @returns the timestamp when the event was sent
+ */
+ getTimestamp(event) {
+ 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.
+ */
+ async handleEvent(event, request, isLiveEvent = false) {
+ const type = event.getType();
+ const content = event.getContent();
+ if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.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(_VerificationRequest.CANCEL_TYPE, (0, _Error.errorFromEvent)((0, _Error.newUnexpectedMessageError)()));
+ return this.sendToDevices(_VerificationRequest.CANCEL_TYPE, cancelContent, [deviceId]);
+ }
+ }
+ const wasStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY;
+ await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
+ const isStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY;
+ const isAcceptingEvent = type === _VerificationRequest.START_TYPE || type === _VerificationRequest.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(_VerificationRequest.CANCEL_TYPE, {
+ code: "m.accepted",
+ reason: "Verification request accepted by another device"
+ });
+ await this.sendToDevices(_VerificationRequest.CANCEL_TYPE, message, nonChosenDevices);
+ }
+ }
+ }
+
+ /**
+ * See {@link InRoomChannel#completedContentFromEvent} for why this is needed.
+ * @param event - the received event
+ * @returns the content object
+ */
+ completedContentFromEvent(event) {
+ 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.
+ */
+ completeContent(type, content) {
+ // make a copy
+ content = Object.assign({}, content);
+ if (this.transactionId) {
+ content.transaction_id = this.transactionId;
+ }
+ if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) {
+ content.from_device = this.client.getDeviceId();
+ }
+ if (type === _VerificationRequest.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
+ */
+ send(type, uncompletedContent = {}) {
+ // create transaction id when sending request
+ if ((type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.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
+ */
+ async sendCompleted(type, content) {
+ let result;
+ if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.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 _event.MatrixEvent({
+ sender: this.client.getUserId(),
+ content,
+ type
+ });
+ await this.request.handleEvent(type, remoteEchoEvent, /*isLiveEvent=*/true, /*isRemoteEcho=*/true, /*isSentByUs=*/true);
+ return result;
+ }
+ async sendToDevices(type, content, devices) {
+ if (devices.length) {
+ const deviceMessages = 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
+ */
+ static makeTransactionId() {
+ return (0, _randomstring.randomString)(32);
+ }
+}
+exports.ToDeviceChannel = ToDeviceChannel;
+class ToDeviceRequests {
+ constructor() {
+ _defineProperty(this, "requestsByUserId", new Map());
+ }
+ getRequest(event) {
+ return this.getRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event));
+ }
+ getRequestByChannel(channel) {
+ return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
+ }
+ getRequestBySenderAndTxnId(sender, txnId) {
+ const requestsByTxnId = this.requestsByUserId.get(sender);
+ if (requestsByTxnId) {
+ return requestsByTxnId.get(txnId);
+ }
+ }
+ setRequest(event, request) {
+ this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
+ }
+ setRequestByChannel(channel, request) {
+ this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
+ }
+ setRequestBySenderAndTxnId(sender, txnId, request) {
+ let requestsByTxnId = this.requestsByUserId.get(sender);
+ if (!requestsByTxnId) {
+ requestsByTxnId = new Map();
+ this.requestsByUserId.set(sender, requestsByTxnId);
+ }
+ requestsByTxnId.set(txnId, request);
+ }
+ removeRequest(event) {
+ 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);
+ }
+ }
+ }
+ findRequestInProgress(userId, devices) {
+ const requestsByTxnId = this.requestsByUserId.get(userId);
+ if (requestsByTxnId) {
+ for (const request of requestsByTxnId.values()) {
+ if (request.pending && request.channel.isToDevices(devices)) {
+ return request;
+ }
+ }
+ }
+ }
+ getRequestsInProgress(userId) {
+ const requestsByTxnId = this.requestsByUserId.get(userId);
+ if (requestsByTxnId) {
+ return Array.from(requestsByTxnId.values()).filter(r => r.pending);
+ }
+ return [];
+ }
+}
+exports.ToDeviceRequests = ToDeviceRequests; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js
new file mode 100644
index 0000000000..d7987f367f
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js
@@ -0,0 +1,870 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.VerificationRequestEvent = exports.VerificationRequest = exports.START_TYPE = exports.REQUEST_TYPE = exports.READY_TYPE = exports.Phase = exports.PHASE_UNSENT = exports.PHASE_STARTED = exports.PHASE_REQUESTED = exports.PHASE_READY = exports.PHASE_DONE = exports.PHASE_CANCELLED = exports.EVENT_PREFIX = exports.DONE_TYPE = exports.CANCEL_TYPE = void 0;
+var _logger = require("../../../logger");
+var _Error = require("../Error");
+var _QRCode = require("../QRCode");
+var _event = require("../../../@types/event");
+var _typedEventEmitter = require("../../../models/typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+// 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
+
+const EVENT_PREFIX = "m.key.verification.";
+exports.EVENT_PREFIX = EVENT_PREFIX;
+const REQUEST_TYPE = EVENT_PREFIX + "request";
+exports.REQUEST_TYPE = REQUEST_TYPE;
+const START_TYPE = EVENT_PREFIX + "start";
+exports.START_TYPE = START_TYPE;
+const CANCEL_TYPE = EVENT_PREFIX + "cancel";
+exports.CANCEL_TYPE = CANCEL_TYPE;
+const DONE_TYPE = EVENT_PREFIX + "done";
+exports.DONE_TYPE = DONE_TYPE;
+const READY_TYPE = EVENT_PREFIX + "ready";
+exports.READY_TYPE = READY_TYPE;
+let Phase = /*#__PURE__*/function (Phase) {
+ Phase[Phase["Unsent"] = 1] = "Unsent";
+ Phase[Phase["Requested"] = 2] = "Requested";
+ Phase[Phase["Ready"] = 3] = "Ready";
+ Phase[Phase["Started"] = 4] = "Started";
+ Phase[Phase["Cancelled"] = 5] = "Cancelled";
+ Phase[Phase["Done"] = 6] = "Done";
+ return Phase;
+}({}); // Legacy export fields
+exports.Phase = Phase;
+const PHASE_UNSENT = Phase.Unsent;
+exports.PHASE_UNSENT = PHASE_UNSENT;
+const PHASE_REQUESTED = Phase.Requested;
+exports.PHASE_REQUESTED = PHASE_REQUESTED;
+const PHASE_READY = Phase.Ready;
+exports.PHASE_READY = PHASE_READY;
+const PHASE_STARTED = Phase.Started;
+exports.PHASE_STARTED = PHASE_STARTED;
+const PHASE_CANCELLED = Phase.Cancelled;
+exports.PHASE_CANCELLED = PHASE_CANCELLED;
+const PHASE_DONE = Phase.Done;
+exports.PHASE_DONE = PHASE_DONE;
+let VerificationRequestEvent = /*#__PURE__*/function (VerificationRequestEvent) {
+ VerificationRequestEvent["Change"] = "change";
+ return VerificationRequestEvent;
+}({});
+exports.VerificationRequestEvent = VerificationRequestEvent;
+/**
+ * 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`.
+ */
+class VerificationRequest extends _typedEventEmitter.TypedEventEmitter {
+ constructor(channel, verificationMethods, client) {
+ super();
+ this.channel = channel;
+ this.verificationMethods = verificationMethods;
+ this.client = client;
+ _defineProperty(this, "eventsByUs", new Map());
+ _defineProperty(this, "eventsByThem", new Map());
+ _defineProperty(this, "_observeOnly", false);
+ _defineProperty(this, "timeoutTimer", null);
+ _defineProperty(this, "_accepting", false);
+ _defineProperty(this, "_declining", false);
+ _defineProperty(this, "verifierHasFinished", false);
+ _defineProperty(this, "_cancelled", false);
+ _defineProperty(this, "_chosenMethod", 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
+ _defineProperty(this, "_qrCodeData", null);
+ // The timestamp when we received the request event from the other side
+ _defineProperty(this, "requestReceivedAt", null);
+ _defineProperty(this, "commonMethods", []);
+ _defineProperty(this, "_phase", void 0);
+ _defineProperty(this, "_cancellingUserId", void 0);
+ // Used in tests only
+ _defineProperty(this, "_verifier", void 0);
+ _defineProperty(this, "cancelOnTimeout", async () => {
+ 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.logger.error("Error while cancelling verification request", err);
+ }
+ });
+ 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
+ */
+ static validateEvent(type, event, client) {
+ 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.logger.log("VerificationRequest: validateEvent: no content");
+ return false;
+ }
+ if (type === REQUEST_TYPE || type === READY_TYPE) {
+ if (!Array.isArray(content.methods)) {
+ _logger.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.logger.log("VerificationRequest: validateEvent: " + "fail because from_device");
+ return false;
+ }
+ }
+ return true;
+ }
+ get invalid() {
+ return this.phase === PHASE_UNSENT;
+ }
+
+ /** returns whether the phase is PHASE_REQUESTED */
+ get requested() {
+ return this.phase === PHASE_REQUESTED;
+ }
+
+ /** returns whether the phase is PHASE_CANCELLED */
+ get cancelled() {
+ return this.phase === PHASE_CANCELLED;
+ }
+
+ /** returns whether the phase is PHASE_READY */
+ get ready() {
+ return this.phase === PHASE_READY;
+ }
+
+ /** returns whether the phase is PHASE_STARTED */
+ get started() {
+ return this.phase === PHASE_STARTED;
+ }
+
+ /** returns whether the phase is PHASE_DONE */
+ get done() {
+ return this.phase === PHASE_DONE;
+ }
+
+ /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
+ get methods() {
+ return this.commonMethods;
+ }
+
+ /** the method picked in the .start event */
+ get chosenMethod() {
+ return this._chosenMethod;
+ }
+ calculateEventTimeout(event) {
+ 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 */
+ get timeout() {
+ 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.
+ */
+ get requestEvent() {
+ return this.getEventByEither(REQUEST_TYPE);
+ }
+
+ /** current phase of the request. Some properties might only be defined in a current phase. */
+ get 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. */
+ get verifier() {
+ return this._verifier;
+ }
+ get canAccept() {
+ return this.phase < PHASE_READY && !this._accepting && !this._declining;
+ }
+ get accepting() {
+ return this._accepting;
+ }
+ get declining() {
+ return this._declining;
+ }
+
+ /** whether this request has sent it's initial event and needs more events to complete */
+ get pending() {
+ 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 */
+ get qrCodeData() {
+ 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 */
+ otherPartySupportsMethod(method, force = false) {
+ 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
+ */
+ get initiatedByMe() {
+ // 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 */
+ get requestingUserId() {
+ if (this.initiatedByMe) {
+ return this.client.getUserId();
+ } else {
+ return this.otherUserId;
+ }
+ }
+
+ /** The id of the user that (will) receive(d) the request */
+ get receivingUserId() {
+ if (this.initiatedByMe) {
+ return this.otherUserId;
+ } else {
+ return this.client.getUserId();
+ }
+ }
+
+ /** The user id of the other party in this request */
+ get otherUserId() {
+ return this.channel.userId;
+ }
+ get isSelfVerification() {
+ return this.client.getUserId() === this.otherUserId;
+ }
+
+ /**
+ * The id of the user that cancelled the request,
+ * only defined when phase is PHASE_CANCELLED
+ */
+ get cancellingUserId() {
+ 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
+ */
+ get cancellationCode() {
+ const ev = this.getEventByEither(CANCEL_TYPE);
+ return ev ? ev.getContent().code : null;
+ }
+ get observeOnly() {
+ 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
+ */
+ get targetDevice() {
+ 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
+ */
+ beginKeyVerification(method, targetDevice = null) {
+ // 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 (0, _Error.newUnknownMethodError)();
+ }
+ this._verifier = this.createVerifier(method, null, targetDevice);
+ if (!this._verifier) {
+ throw (0, _Error.newUnknownMethodError)();
+ }
+ this._chosenMethod = method;
+ }
+ }
+ return this._verifier;
+ }
+
+ /**
+ * sends the initial .request event.
+ * @returns resolves when the event has been sent.
+ */
+ async sendRequest() {
+ 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.
+ */
+ async cancel({
+ reason = "User declined",
+ code = "m.user"
+ } = {}) {
+ if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
+ this._declining = true;
+ this.emit(VerificationRequestEvent.Change);
+ if (this._verifier) {
+ return this._verifier.cancel((0, _Error.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.
+ */
+ async accept() {
+ 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
+ */
+ waitFor(fn) {
+ return new Promise((resolve, reject) => {
+ const check = () => {
+ 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);
+ }
+ });
+ }
+ setPhase(phase, notify = true) {
+ this._phase = phase;
+ if (notify) {
+ this.emit(VerificationRequestEvent.Change);
+ }
+ }
+ getEventByEither(type) {
+ return this.eventsByThem.get(type) || this.eventsByUs.get(type);
+ }
+ getEventBy(type, byThem = false) {
+ if (byThem) {
+ return this.eventsByThem.get(type);
+ } else {
+ return this.eventsByUs.get(type);
+ }
+ }
+ calculatePhaseTransitions() {
+ const transitions = [{
+ phase: PHASE_UNSENT
+ }];
+ const 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;
+ 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;
+ }
+ transitionToPhase(transition) {
+ const {
+ phase,
+ event
+ } = transition;
+ // get common methods
+ if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
+ if (!this.wasSentByOwnDevice(event)) {
+ const content = event.getContent();
+ 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;
+ }
+ }
+ }
+ }
+ applyPhaseTransitions() {
+ 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;
+ }
+ isWinningStartRace(newEvent) {
+ 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;
+ }
+ hasEventId(eventId) {
+ 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.
+ */
+ async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
+ // 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(_QRCode.SCAN_QR_CODE_METHOD, true);
+ if (shouldGenerateQrCode) {
+ this._qrCodeData = await _QRCode.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.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}`);
+ }
+ }
+ setupTimeout(phase) {
+ 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;
+ }
+ }
+ }
+ async cancelOnError(type, event) {
+ if (type === START_TYPE) {
+ const method = event.getContent().method;
+ if (!this.verificationMethods.has(method)) {
+ await this.cancel((0, _Error.errorFromEvent)((0, _Error.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.logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`);
+ const reason = `Unexpected ${type} event in phase ${this.phase}`;
+ await this.cancel((0, _Error.errorFromEvent)((0, _Error.newUnexpectedMessageError)({
+ reason
+ })));
+ return true;
+ }
+ return false;
+ }
+ adjustObserveOnly(event, isLiveEvent = false) {
+ // don't send out events for historical requests
+ if (!isLiveEvent) {
+ this._observeOnly = true;
+ }
+ if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
+ this._observeOnly = true;
+ }
+ }
+ addEvent(type, event, isSentByUs = false) {
+ 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();
+ }
+ }
+ createVerifier(method, startEvent = null, targetDevice = null) {
+ if (!targetDevice) {
+ targetDevice = this.targetDevice;
+ }
+ const {
+ userId,
+ deviceId
+ } = targetDevice;
+ const VerifierCtor = this.verificationMethods.get(method);
+ if (!VerifierCtor) {
+ _logger.logger.warn("could not find verifier constructor for method", method);
+ return;
+ }
+ return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
+ }
+ wasSentByOwnUser(event) {
+ return event?.getSender() === this.client.getUserId();
+ }
+
+ // only for .request, .ready or .start
+ wasSentByOwnDevice(event) {
+ if (!this.wasSentByOwnUser(event)) {
+ return false;
+ }
+ const content = event.getContent();
+ if (!content || content.from_device !== this.client.getDeviceId()) {
+ return false;
+ }
+ return true;
+ }
+ onVerifierCancelled() {
+ this._cancelled = true;
+ // move to cancelled phase
+ const newTransitions = this.applyPhaseTransitions();
+ if (newTransitions.length) {
+ this.setPhase(newTransitions[newTransitions.length - 1].phase);
+ }
+ }
+ onVerifierFinished() {
+ this.channel.send(_event.EventType.KeyVerificationDone, {});
+ this.verifierHasFinished = true;
+ // move to .done phase
+ const newTransitions = this.applyPhaseTransitions();
+ if (newTransitions.length) {
+ this.setPhase(newTransitions[newTransitions.length - 1].phase);
+ }
+ }
+ getEventFromOtherParty(type) {
+ return this.eventsByThem.get(type);
+ }
+}
+exports.VerificationRequest = VerificationRequest; \ No newline at end of file