summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/CrossSigningIdentity.js93
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js78
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js117
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js124
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js31
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js25
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/device-converter.js121
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js54
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js574
9 files changed, 1217 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/CrossSigningIdentity.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/CrossSigningIdentity.js
new file mode 100644
index 0000000000..3bb17a0cef
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/CrossSigningIdentity.js
@@ -0,0 +1,93 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CrossSigningIdentity = void 0;
+var _logger = require("../logger");
+/*
+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.
+*/
+
+/** Manages the cross-signing keys for our own user.
+ */
+class CrossSigningIdentity {
+ constructor(olmMachine, outgoingRequestProcessor) {
+ this.olmMachine = olmMachine;
+ this.outgoingRequestProcessor = outgoingRequestProcessor;
+ }
+
+ /**
+ * Initialise our cross-signing keys by creating new keys if they do not exist, and uploading to the server
+ */
+ async bootstrapCrossSigning(opts) {
+ if (opts.setupNewCrossSigning) {
+ await this.resetCrossSigning(opts.authUploadDeviceSigningKeys);
+ return;
+ }
+ const olmDeviceStatus = await this.olmMachine.crossSigningStatus();
+ const privateKeysInSecretStorage = false; // TODO
+ const olmDeviceHasKeys = olmDeviceStatus.hasMaster && olmDeviceStatus.hasUserSigning && olmDeviceStatus.hasSelfSigning;
+
+ // Log all relevant state for easier parsing of debug logs.
+ _logger.logger.log("bootStrapCrossSigning: starting", {
+ setupNewCrossSigning: opts.setupNewCrossSigning,
+ olmDeviceHasMaster: olmDeviceStatus.hasMaster,
+ olmDeviceHasUserSigning: olmDeviceStatus.hasUserSigning,
+ olmDeviceHasSelfSigning: olmDeviceStatus.hasSelfSigning,
+ privateKeysInSecretStorage
+ });
+ if (!olmDeviceHasKeys && !privateKeysInSecretStorage) {
+ _logger.logger.log("bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys");
+ await this.resetCrossSigning(opts.authUploadDeviceSigningKeys);
+ } else if (olmDeviceHasKeys) {
+ _logger.logger.log("bootStrapCrossSigning: Olm device has private keys: exporting to secret storage");
+ await this.exportCrossSigningKeysToStorage();
+ } else if (privateKeysInSecretStorage) {
+ _logger.logger.log("bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " + "in secret storage, reading storage and caching locally");
+ throw new Error("TODO");
+ }
+
+ // TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the
+ // server -- in which case we should call OlmDevice.bootstrap_cross_signing. How do we know?
+ _logger.logger.log("bootStrapCrossSigning: complete");
+ }
+
+ /** Reset our cross-signing keys
+ *
+ * This method will:
+ * * Tell the OlmMachine to create new keys
+ * * Upload the new public keys and the device signature to the server
+ * * Upload the private keys to SSSS, if it is set up
+ */
+ async resetCrossSigning(authUploadDeviceSigningKeys) {
+ const outgoingRequests = await this.olmMachine.bootstrapCrossSigning(true);
+ _logger.logger.log("bootStrapCrossSigning: publishing keys to server");
+ for (const req of outgoingRequests) {
+ await this.outgoingRequestProcessor.makeOutgoingRequest(req, authUploadDeviceSigningKeys);
+ }
+ await this.exportCrossSigningKeysToStorage();
+ }
+
+ /**
+ * Extract the cross-signing keys from the olm machine and save them to secret storage, if it is configured
+ *
+ * (If secret storage is *not* configured, we assume that the export will happen when it is set up)
+ */
+ async exportCrossSigningKeysToStorage() {
+ // TODO
+ }
+}
+exports.CrossSigningIdentity = CrossSigningIdentity; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js
new file mode 100644
index 0000000000..a560259504
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js
@@ -0,0 +1,78 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.KeyClaimManager = void 0;
+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 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.
+*/
+
+/**
+ * KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races
+ *
+ * We have one of these per `RustCrypto` (and hence per `MatrixClient`).
+ */
+class KeyClaimManager {
+ constructor(olmMachine, outgoingRequestProcessor) {
+ this.olmMachine = olmMachine;
+ this.outgoingRequestProcessor = outgoingRequestProcessor;
+ _defineProperty(this, "currentClaimPromise", void 0);
+ _defineProperty(this, "stopped", false);
+ this.currentClaimPromise = Promise.resolve();
+ }
+
+ /**
+ * Tell the KeyClaimManager to immediately stop processing requests.
+ *
+ * Any further calls, and any still in the queue, will fail with an error.
+ */
+ stop() {
+ this.stopped = true;
+ }
+
+ /**
+ * Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices
+ *
+ * If we don't have an active olm session, we will claim a one-time key and start one.
+ *
+ * @param userList - list of userIDs to claim
+ */
+ ensureSessionsForUsers(userList) {
+ // The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance
+ // ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them
+ // queue up in order).
+ const prom = this.currentClaimPromise.catch(() => {
+ // any errors in the previous claim will have been reported already, so there is nothing to do here.
+ // we just throw away the error and start anew.
+ }).then(() => this.ensureSessionsForUsersInner(userList));
+ this.currentClaimPromise = prom;
+ return prom;
+ }
+ async ensureSessionsForUsersInner(userList) {
+ // bail out quickly if we've been stopped.
+ if (this.stopped) {
+ throw new Error(`Cannot ensure Olm sessions: shutting down`);
+ }
+ const claimRequest = await this.olmMachine.getMissingSessions(userList);
+ if (claimRequest) {
+ await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest);
+ }
+ }
+}
+exports.KeyClaimManager = KeyClaimManager; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js
new file mode 100644
index 0000000000..cbf10b51ba
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js
@@ -0,0 +1,117 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.OutgoingRequestProcessor = void 0;
+var _matrixSdkCryptoJs = require("@matrix-org/matrix-sdk-crypto-js");
+var _logger = require("../logger");
+var _httpApi = require("../http-api");
+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 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.
+ */
+/**
+ * Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
+ */
+
+/**
+ * OutgoingRequestManager: turns `OutgoingRequest`s from the rust sdk into HTTP requests
+ *
+ * We have one of these per `RustCrypto` (and hence per `MatrixClient`), not that it does anything terribly complicated.
+ * It's responsible for:
+ *
+ * * holding the reference to the `MatrixHttpApi`
+ * * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them
+ * * sending the results of such requests back to the rust backend.
+ */
+class OutgoingRequestProcessor {
+ constructor(olmMachine, http) {
+ this.olmMachine = olmMachine;
+ this.http = http;
+ }
+ async makeOutgoingRequest(msg, uiaCallback) {
+ let resp;
+
+ /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
+ * for the complete list of request types
+ */
+ if (msg instanceof _matrixSdkCryptoJs.KeysUploadRequest) {
+ resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.KeysQueryRequest) {
+ resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.KeysClaimRequest) {
+ resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.SignatureUploadRequest) {
+ resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.KeysBackupRequest) {
+ resp = await this.rawJsonRequest(_httpApi.Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.ToDeviceRequest) {
+ const path = `/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` + encodeURIComponent(msg.txn_id);
+ resp = await this.rawJsonRequest(_httpApi.Method.Put, path, {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.RoomMessageRequest) {
+ const path = `/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` + `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
+ resp = await this.rawJsonRequest(_httpApi.Method.Put, path, {}, msg.body);
+ } else if (msg instanceof _matrixSdkCryptoJs.SigningKeysUploadRequest) {
+ resp = await this.makeRequestWithUIA(_httpApi.Method.Post, "/_matrix/client/v3/keys/device_signing/upload", {}, msg.body, uiaCallback);
+ } else {
+ _logger.logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
+ resp = "";
+ }
+ if (msg.id) {
+ await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
+ }
+ }
+ async makeRequestWithUIA(method, path, queryParams, body, uiaCallback) {
+ if (!uiaCallback) {
+ return await this.rawJsonRequest(method, path, queryParams, body);
+ }
+ const parsedBody = JSON.parse(body);
+ const makeRequest = async auth => {
+ const newBody = _objectSpread(_objectSpread({}, parsedBody), {}, {
+ auth
+ });
+ const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody));
+ return JSON.parse(resp);
+ };
+ const resp = await uiaCallback(makeRequest);
+ return JSON.stringify(resp);
+ }
+ async rawJsonRequest(method, path, queryParams, body) {
+ const opts = {
+ // inhibit the JSON stringification and parsing within HttpApi.
+ json: false,
+ // nevertheless, we are sending, and accept, JSON.
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ },
+ // we use the full prefix
+ prefix: ""
+ };
+ try {
+ const response = await this.http.authedRequest(method, path, queryParams, body, opts);
+ _logger.logger.info(`rust-crypto: successfully made HTTP request: ${method} ${path}`);
+ return response;
+ } catch (e) {
+ _logger.logger.warn(`rust-crypto: error making HTTP request: ${method} ${path}: ${e}`);
+ throw e;
+ }
+ }
+}
+exports.OutgoingRequestProcessor = OutgoingRequestProcessor; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js
new file mode 100644
index 0000000000..48a8665761
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js
@@ -0,0 +1,124 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomEncryptor = void 0;
+var _matrixSdkCryptoJs = require("@matrix-org/matrix-sdk-crypto-js");
+var _event = require("../@types/event");
+var _logger = require("../logger");
+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 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.
+ */
+/**
+ * RoomEncryptor: responsible for encrypting messages to a given room
+ */
+class RoomEncryptor {
+ /**
+ * @param olmMachine - The rust-sdk's OlmMachine
+ * @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests
+ * @param room - The room we want to encrypt for
+ * @param encryptionSettings - body of the m.room.encryption event currently in force in this room
+ */
+ constructor(olmMachine, keyClaimManager, outgoingRequestProcessor, room, encryptionSettings) {
+ this.olmMachine = olmMachine;
+ this.keyClaimManager = keyClaimManager;
+ this.outgoingRequestProcessor = outgoingRequestProcessor;
+ this.room = room;
+ this.encryptionSettings = encryptionSettings;
+ _defineProperty(this, "prefixedLogger", void 0);
+ this.prefixedLogger = _logger.logger.withPrefix(`[${room.roomId} encryption]`);
+ }
+
+ /**
+ * Handle a new `m.room.encryption` event in this room
+ *
+ * @param config - The content of the encryption event
+ */
+ onCryptoEvent(config) {
+ if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
+ this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
+ }
+ }
+
+ /**
+ * Handle a new `m.room.member` event in this room
+ *
+ * @param member - new membership state
+ */
+ onRoomMembership(member) {
+ this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`);
+ if (member.membership == "join" || member.membership == "invite" && this.room.shouldEncryptForInvitedMembers()) {
+ // make sure we are tracking the deviceList for this user
+ this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`);
+ this.olmMachine.updateTrackedUsers([new _matrixSdkCryptoJs.UserId(member.userId)]);
+ }
+
+ // TODO: handle leaves (including our own)
+ }
+
+ /**
+ * Prepare to encrypt events in this room.
+ *
+ * This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
+ * in the room.
+ */
+ async ensureEncryptionSession() {
+ if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
+ throw new Error(`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`);
+ }
+ const members = await this.room.getEncryptionTargetMembers();
+ this.prefixedLogger.debug(`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, members.map(u => `${u.userId} (${u.membership})`));
+ const userList = members.map(u => new _matrixSdkCryptoJs.UserId(u.userId));
+ await this.keyClaimManager.ensureSessionsForUsers(userList);
+ this.prefixedLogger.debug("Sessions for users are ready; now sharing room key");
+ const rustEncryptionSettings = new _matrixSdkCryptoJs.EncryptionSettings();
+ /* FIXME historyVisibility, rotation, etc */
+
+ const shareMessages = await this.olmMachine.shareRoomKey(new _matrixSdkCryptoJs.RoomId(this.room.roomId), userList, rustEncryptionSettings);
+ if (shareMessages) {
+ for (const m of shareMessages) {
+ await this.outgoingRequestProcessor.makeOutgoingRequest(m);
+ }
+ }
+ }
+
+ /**
+ * Discard any existing group session for this room
+ */
+ async forceDiscardSession() {
+ const r = await this.olmMachine.invalidateGroupSession(new _matrixSdkCryptoJs.RoomId(this.room.roomId));
+ if (r) {
+ this.prefixedLogger.info("Discarded existing group session");
+ }
+ }
+
+ /**
+ * Encrypt an event for this room
+ *
+ * This will ensure that we have a megolm session for this room, share it with the devices in the room, and
+ * then encrypt the event using the session.
+ *
+ * @param event - Event to be encrypted.
+ */
+ async encryptEvent(event) {
+ await this.ensureEncryptionSession();
+ const encryptedContent = await this.olmMachine.encryptRoomEvent(new _matrixSdkCryptoJs.RoomId(this.room.roomId), event.getType(), JSON.stringify(event.getContent()));
+ event.makeEncrypted(_event.EventType.RoomMessageEncrypted, JSON.parse(encryptedContent), this.olmMachine.identityKeys.curve25519.toBase64(), this.olmMachine.identityKeys.ed25519.toBase64());
+ }
+}
+exports.RoomEncryptor = RoomEncryptor; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js
new file mode 100644
index 0000000000..5876cbad6a
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js
@@ -0,0 +1,31 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.initRustCrypto = initRustCrypto;
+/*
+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.
+*/
+
+/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
+ *
+ * It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
+ * It deliberately does nothing except raise an exception.
+ */
+
+async function initRustCrypto(_http, _userId, _deviceId) {
+ throw new Error("Rust crypto is not supported under browserify.");
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js
new file mode 100644
index 0000000000..cd1599ff0b
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js
@@ -0,0 +1,25 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RUST_SDK_STORE_PREFIX = void 0;
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/** The prefix used on indexeddbs created by rust-crypto */
+const RUST_SDK_STORE_PREFIX = "matrix-js-sdk";
+exports.RUST_SDK_STORE_PREFIX = RUST_SDK_STORE_PREFIX; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/device-converter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/device-converter.js
new file mode 100644
index 0000000000..83623fca0a
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/device-converter.js
@@ -0,0 +1,121 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.deviceKeysToDeviceMap = deviceKeysToDeviceMap;
+exports.downloadDeviceToJsDevice = downloadDeviceToJsDevice;
+exports.rustDeviceToJsDevice = rustDeviceToJsDevice;
+var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-js"));
+var _device = require("../models/device");
+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 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 RustSdkCryptoJs.Device} to a {@link Device}
+ * @param device - Rust Sdk device
+ * @param userId - owner of the device
+ */
+function rustDeviceToJsDevice(device, userId) {
+ // Copy rust device keys to Device.keys
+ const keys = new Map();
+ for (const [keyId, key] of device.keys.entries()) {
+ keys.set(keyId.toString(), key.toBase64());
+ }
+
+ // Compute verified from device state
+ let verified = _device.DeviceVerification.Unverified;
+ if (device.isBlacklisted()) {
+ verified = _device.DeviceVerification.Blocked;
+ } else if (device.isVerified()) {
+ verified = _device.DeviceVerification.Verified;
+ }
+
+ // Convert rust signatures to Device.signatures
+ const signatures = new Map();
+ const mayBeSignatureMap = device.signatures.get(userId);
+ if (mayBeSignatureMap) {
+ const convertedSignatures = new Map();
+ // Convert maybeSignatures map to a Map<string, string>
+ for (const [key, value] of mayBeSignatureMap.entries()) {
+ if (value.isValid() && value.signature) {
+ convertedSignatures.set(key, value.signature.toBase64());
+ }
+ }
+ signatures.set(userId.toString(), convertedSignatures);
+ }
+
+ // Convert rust algorithms to algorithms
+ const rustAlgorithms = device.algorithms;
+ // Use set to ensure that algorithms are not duplicated
+ const algorithms = new Set();
+ rustAlgorithms.forEach(algorithm => {
+ switch (algorithm) {
+ case RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2:
+ algorithms.add("m.megolm.v1.aes-sha2");
+ break;
+ case RustSdkCryptoJs.EncryptionAlgorithm.OlmV1Curve25519AesSha2:
+ default:
+ algorithms.add("m.olm.v1.curve25519-aes-sha2");
+ break;
+ }
+ });
+ return new _device.Device({
+ deviceId: device.deviceId.toString(),
+ userId: userId.toString(),
+ keys,
+ algorithms: Array.from(algorithms),
+ verified,
+ signatures,
+ displayName: device.displayName
+ });
+}
+
+/**
+ * Convert {@link DeviceKeys} from `/keys/query` request to a `Map<string, Device>`
+ * @param deviceKeys - Device keys object to convert
+ */
+function deviceKeysToDeviceMap(deviceKeys) {
+ return new Map(Object.entries(deviceKeys).map(([deviceId, device]) => [deviceId, downloadDeviceToJsDevice(device)]));
+}
+
+// Device from `/keys/query` request
+
+/**
+ * Convert `/keys/query` {@link QueryDevice} device to {@link Device}
+ * @param device - Device from `/keys/query` request
+ */
+function downloadDeviceToJsDevice(device) {
+ const keys = new Map(Object.entries(device.keys));
+ const displayName = device.unsigned?.device_display_name;
+ const signatures = new Map();
+ if (device.signatures) {
+ for (const userId in device.signatures) {
+ signatures.set(userId, new Map(Object.entries(device.signatures[userId])));
+ }
+ }
+ return new _device.Device({
+ deviceId: device.device_id,
+ userId: device.user_id,
+ keys,
+ algorithms: device.algorithms,
+ verified: _device.DeviceVerification.Unverified,
+ signatures,
+ displayName
+ });
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js
new file mode 100644
index 0000000000..2ba47c5f40
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js
@@ -0,0 +1,54 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.initRustCrypto = initRustCrypto;
+var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-js"));
+var _rustCrypto = require("./rust-crypto");
+var _logger = require("../logger");
+var _constants = require("./constants");
+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 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.
+*/
+
+/**
+ * Create a new `RustCrypto` implementation
+ *
+ * @param http - Low-level HTTP interface: used to make outgoing requests required by the rust SDK.
+ * We expect it to set the access token, etc.
+ * @param userId - The local user's User ID.
+ * @param deviceId - The local user's Device ID.
+ * @param secretStorage - Interface to server-side secret storage.
+ */
+async function initRustCrypto(http, userId, deviceId, secretStorage) {
+ // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
+ await RustSdkCryptoJs.initAsync();
+
+ // enable tracing in the rust-sdk
+ new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Trace).turnOn();
+ const u = new RustSdkCryptoJs.UserId(userId);
+ const d = new RustSdkCryptoJs.DeviceId(deviceId);
+ _logger.logger.info("Init OlmMachine");
+
+ // TODO: use the pickle key for the passphrase
+ const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, _constants.RUST_SDK_STORE_PREFIX, "test pass");
+ const rustCrypto = new _rustCrypto.RustCrypto(olmMachine, http, userId, deviceId, secretStorage);
+ await olmMachine.registerRoomKeyUpdatedCallback(sessions => rustCrypto.onRoomKeysUpdated(sessions));
+ _logger.logger.info("Completed rust crypto-sdk setup");
+ return rustCrypto;
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js
new file mode 100644
index 0000000000..b55d2fb64b
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js
@@ -0,0 +1,574 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RustCrypto = void 0;
+var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-js"));
+var _logger = require("../logger");
+var _httpApi = require("../http-api");
+var _CrossSigning = require("../crypto/CrossSigning");
+var _RoomEncryptor = require("./RoomEncryptor");
+var _OutgoingRequestProcessor = require("./OutgoingRequestProcessor");
+var _KeyClaimManager = require("./KeyClaimManager");
+var _utils = require("../utils");
+var _cryptoApi = require("../crypto-api");
+var _deviceConverter = require("./device-converter");
+var _api = require("../crypto/api");
+var _CrossSigningIdentity = require("./CrossSigningIdentity");
+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 2022-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.
+ */
+/**
+ * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
+ */
+class RustCrypto {
+ constructor( /** The `OlmMachine` from the underlying rust crypto sdk. */
+ olmMachine,
+ /**
+ * Low-level HTTP interface: used to make outgoing requests required by the rust SDK.
+ *
+ * We expect it to set the access token, etc.
+ */
+ http, /** The local user's User ID. */
+ _userId, /** The local user's Device ID. */
+ _deviceId, /** Interface to server-side secret storage */
+ _secretStorage) {
+ this.olmMachine = olmMachine;
+ this.http = http;
+ _defineProperty(this, "globalErrorOnUnknownDevices", false);
+ _defineProperty(this, "_trustCrossSignedDevices", true);
+ /** whether {@link stop} has been called */
+ _defineProperty(this, "stopped", false);
+ /** whether {@link outgoingRequestLoop} is currently running */
+ _defineProperty(this, "outgoingRequestLoopRunning", false);
+ /** mapping of roomId → encryptor class */
+ _defineProperty(this, "roomEncryptors", {});
+ _defineProperty(this, "eventDecryptor", void 0);
+ _defineProperty(this, "keyClaimManager", void 0);
+ _defineProperty(this, "outgoingRequestProcessor", void 0);
+ _defineProperty(this, "crossSigningIdentity", void 0);
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // CryptoApi implementation
+ //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ _defineProperty(this, "globalBlacklistUnverifiedDevices", false);
+ this.outgoingRequestProcessor = new _OutgoingRequestProcessor.OutgoingRequestProcessor(olmMachine, http);
+ this.keyClaimManager = new _KeyClaimManager.KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
+ this.eventDecryptor = new EventDecryptor(olmMachine);
+ this.crossSigningIdentity = new _CrossSigningIdentity.CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // CryptoBackend implementation
+ //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ stop() {
+ // stop() may be called multiple times, but attempting to close() the OlmMachine twice
+ // will cause an error.
+ if (this.stopped) {
+ return;
+ }
+ this.stopped = true;
+ this.keyClaimManager.stop();
+
+ // make sure we close() the OlmMachine; doing so means that all the Rust objects will be
+ // cleaned up; in particular, the indexeddb connections will be closed, which means they
+ // can then be deleted.
+ this.olmMachine.close();
+ }
+ async encryptEvent(event, _room) {
+ const roomId = event.getRoomId();
+ const encryptor = this.roomEncryptors[roomId];
+ if (!encryptor) {
+ throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
+ }
+ await encryptor.encryptEvent(event);
+ }
+ async decryptEvent(event) {
+ const roomId = event.getRoomId();
+ if (!roomId) {
+ // presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages
+ // so the fact it has come back here suggests that decryption failed.
+ //
+ // once we drop support for the libolm crypto implementation, we can stop passing to-device messages
+ // through decryptEvent and hence get rid of this case.
+ throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
+ }
+ return await this.eventDecryptor.attemptEventDecryption(event);
+ }
+ getEventEncryptionInfo(event) {
+ // TODO: make this work properly. Or better, replace it.
+
+ 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;
+ ret.authenticated = true;
+ ret.mismatchedSender = true;
+ return ret;
+ }
+ checkUserTrust(userId) {
+ // TODO
+ return new _CrossSigning.UserTrustLevel(false, false, false);
+ }
+
+ /**
+ * Finds a DM verification request that is already in progress for the given room id
+ *
+ * @param roomId - the room to use for verification
+ *
+ * @returns the VerificationRequest that is in progress, if any
+ */
+ findVerificationRequestDMInProgress(roomId) {
+ // TODO
+ return;
+ }
+
+ /**
+ * Get the cross signing information for a given user.
+ *
+ * The cross-signing API is currently UNSTABLE and may change without notice.
+ *
+ * @param userId - the user ID to get the cross-signing info for.
+ *
+ * @returns the cross signing information for the user.
+ */
+ getStoredCrossSigningForUser(userId) {
+ // TODO
+ return null;
+ }
+ async userHasCrossSigningKeys() {
+ // TODO
+ return false;
+ }
+ prepareToEncrypt(room) {
+ const encryptor = this.roomEncryptors[room.roomId];
+ if (encryptor) {
+ encryptor.ensureEncryptionSession();
+ }
+ }
+ forceDiscardSession(roomId) {
+ return this.roomEncryptors[roomId]?.forceDiscardSession();
+ }
+ async exportRoomKeys() {
+ // TODO
+ return [];
+ }
+
+ /**
+ * 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();
+ const rustTrackedUsers = await this.olmMachine.trackedUsers();
+
+ // Convert RustSdkCryptoJs.UserId to a `Set<string>`
+ const trackedUsers = new Set();
+ rustTrackedUsers.forEach(rustUserId => trackedUsers.add(rustUserId.toString()));
+
+ // Keep untracked user to download their keys after
+ const untrackedUsers = new Set();
+ for (const userId of userIds) {
+ // if this is a tracked user, we can just fetch the device list from the rust-sdk
+ // (NB: this is probably ok even if we race with a leave event such that we stop tracking the user's
+ // devices: the rust-sdk will return the last-known device list, which will be good enough.)
+ if (trackedUsers.has(userId)) {
+ deviceMapByUserId.set(userId, await this.getUserDevices(userId));
+ } else {
+ untrackedUsers.add(userId);
+ }
+ }
+
+ // for any users whose device lists we are not tracking, fall back to downloading the device list
+ // over HTTP.
+ if (downloadUncached && untrackedUsers.size >= 1) {
+ const queryResult = await this.downloadDeviceList(untrackedUsers);
+ Object.entries(queryResult.device_keys).forEach(([userId, deviceKeys]) => deviceMapByUserId.set(userId, (0, _deviceConverter.deviceKeysToDeviceMap)(deviceKeys)));
+ }
+ return deviceMapByUserId;
+ }
+
+ /**
+ * Get the device list for the given user from the olm machine
+ * @param userId - Rust SDK UserId
+ */
+ async getUserDevices(userId) {
+ const rustUserId = new RustSdkCryptoJs.UserId(userId);
+ const devices = await this.olmMachine.getUserDevices(rustUserId);
+ return new Map(devices.devices().map(device => [device.deviceId.toString(), (0, _deviceConverter.rustDeviceToJsDevice)(device, rustUserId)]));
+ }
+
+ /**
+ * Download the given user keys by calling `/keys/query` request
+ * @param untrackedUsers - download keys of these users
+ */
+ async downloadDeviceList(untrackedUsers) {
+ const queryBody = {
+ device_keys: {}
+ };
+ untrackedUsers.forEach(user => queryBody.device_keys[user] = []);
+ return await this.http.authedRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/query", undefined, queryBody, {
+ prefix: ""
+ });
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#getTrustCrossSignedDevices}.
+ */
+ getTrustCrossSignedDevices() {
+ return this._trustCrossSignedDevices;
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#setTrustCrossSignedDevices}.
+ */
+ setTrustCrossSignedDevices(val) {
+ this._trustCrossSignedDevices = val;
+ // TODO: legacy crypto goes through the list of known devices and emits DeviceVerificationChanged
+ // events. Maybe we need to do the same?
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#getDeviceVerificationStatus}.
+ */
+ async getDeviceVerificationStatus(userId, deviceId) {
+ const device = await this.olmMachine.getDevice(new RustSdkCryptoJs.UserId(userId), new RustSdkCryptoJs.DeviceId(deviceId));
+ if (!device) return null;
+ return new _cryptoApi.DeviceVerificationStatus({
+ signedByOwner: device.isCrossSignedByOwner(),
+ crossSigningVerified: device.isCrossSigningTrusted(),
+ localVerified: device.isLocallyTrusted(),
+ trustCrossSignedDevices: this._trustCrossSignedDevices
+ });
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#isCrossSigningReady}
+ */
+ async isCrossSigningReady() {
+ return false;
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#getCrossSigningKeyId}
+ */
+ async getCrossSigningKeyId(type = _api.CrossSigningKey.Master) {
+ // TODO
+ return null;
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#boostrapCrossSigning}
+ */
+ async bootstrapCrossSigning(opts) {
+ await this.crossSigningIdentity.bootstrapCrossSigning(opts);
+ }
+
+ /**
+ * Implementation of {@link CryptoApi#isSecretStorageReady}
+ */
+ async isSecretStorageReady() {
+ return false;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // SyncCryptoCallbacks implementation
+ //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Apply sync changes to the olm machine
+ * @param events - the received to-device messages
+ * @param oneTimeKeysCounts - the received one time key counts
+ * @param unusedFallbackKeys - the received unused fallback keys
+ * @param devices - the received device list updates
+ * @returns A list of preprocessed to-device messages.
+ */
+ async receiveSyncChanges({
+ events,
+ oneTimeKeysCounts = new Map(),
+ unusedFallbackKeys,
+ devices = new RustSdkCryptoJs.DeviceLists()
+ }) {
+ const result = await this.olmMachine.receiveSyncChanges(events ? JSON.stringify(events) : "[]", devices, oneTimeKeysCounts, unusedFallbackKeys);
+
+ // receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
+ return JSON.parse(result);
+ }
+
+ /** called by the sync loop to preprocess incoming to-device messages
+ *
+ * @param events - the received to-device messages
+ * @returns A list of preprocessed to-device messages.
+ */
+ preprocessToDeviceMessages(events) {
+ // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
+ // one-time-keys, or fallback keys, so just pass empty data.
+ return this.receiveSyncChanges({
+ events
+ });
+ }
+
+ /** called by the sync loop to process one time key counts and unused fallback keys
+ *
+ * @param oneTimeKeysCounts - the received one time key counts
+ * @param unusedFallbackKeys - the received unused fallback keys
+ */
+ async processKeyCounts(oneTimeKeysCounts, unusedFallbackKeys) {
+ const mapOneTimeKeysCount = oneTimeKeysCounts && new Map(Object.entries(oneTimeKeysCounts));
+ const setUnusedFallbackKeys = unusedFallbackKeys && new Set(unusedFallbackKeys);
+ if (mapOneTimeKeysCount !== undefined || setUnusedFallbackKeys !== undefined) {
+ await this.receiveSyncChanges({
+ oneTimeKeysCounts: mapOneTimeKeysCount,
+ unusedFallbackKeys: setUnusedFallbackKeys
+ });
+ }
+ }
+
+ /** called by the sync loop to process the notification that device lists have
+ * been changed.
+ *
+ * @param deviceLists - device_lists field from /sync
+ */
+ async processDeviceLists(deviceLists) {
+ const devices = new RustSdkCryptoJs.DeviceLists(deviceLists.changed?.map(userId => new RustSdkCryptoJs.UserId(userId)), deviceLists.left?.map(userId => new RustSdkCryptoJs.UserId(userId)));
+ await this.receiveSyncChanges({
+ devices
+ });
+ }
+
+ /** called by the sync loop on m.room.encrypted events
+ *
+ * @param room - in which the event was received
+ * @param event - encryption event to be processed
+ */
+ async onCryptoEvent(room, event) {
+ const config = event.getContent();
+ const existingEncryptor = this.roomEncryptors[room.roomId];
+ if (existingEncryptor) {
+ existingEncryptor.onCryptoEvent(config);
+ } else {
+ this.roomEncryptors[room.roomId] = new _RoomEncryptor.RoomEncryptor(this.olmMachine, this.keyClaimManager, this.outgoingRequestProcessor, room, config);
+ }
+
+ // start tracking devices for any users already known to be in this room.
+ const members = await room.getEncryptionTargetMembers();
+ _logger.logger.debug(`[${room.roomId} encryption] starting to track devices for: `, members.map(u => `${u.userId} (${u.membership})`));
+ await this.olmMachine.updateTrackedUsers(members.map(u => new RustSdkCryptoJs.UserId(u.userId)));
+ }
+
+ /** called by the sync loop after processing each sync.
+ *
+ * TODO: figure out something equivalent for sliding sync.
+ *
+ * @param syncState - information on the completed sync.
+ */
+ onSyncCompleted(syncState) {
+ // Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing
+ // request loop, if it's not already running.
+ this.outgoingRequestLoop();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Other public functions
+ //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /** called by the MatrixClient on a room membership event
+ *
+ * @param event - The matrix event which caused this event to fire.
+ * @param member - The member whose RoomMember.membership changed.
+ * @param oldMembership - The previous membership state. Null if it's a new member.
+ */
+ onRoomMembership(event, member, oldMembership) {
+ const enc = this.roomEncryptors[event.getRoomId()];
+ if (!enc) {
+ // not encrypting in this room
+ return;
+ }
+ enc.onRoomMembership(member);
+ }
+
+ /** Callback for OlmMachine.registerRoomKeyUpdatedCallback
+ *
+ * Called by the rust-sdk whenever there is an update to (megolm) room keys. We
+ * check if we have any events waiting for the given keys, and schedule them for
+ * a decryption retry if so.
+ *
+ * @param keys - details of the updated keys
+ */
+ async onRoomKeysUpdated(keys) {
+ for (const key of keys) {
+ this.onRoomKeyUpdated(key);
+ }
+ }
+ onRoomKeyUpdated(key) {
+ _logger.logger.debug(`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`);
+ const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
+ if (pendingList.length === 0) return;
+ _logger.logger.debug("Retrying decryption on events:", pendingList.map(e => `${e.getId()}`));
+
+ // Have another go at decrypting events with this key.
+ //
+ // We don't want to end up blocking the callback from Rust, which could otherwise end up dropping updates,
+ // so we don't wait for the decryption to complete. In any case, there is no need to wait:
+ // MatrixEvent.attemptDecryption ensures that there is only one decryption attempt happening at once,
+ // and deduplicates repeated attempts for the same event.
+ for (const ev of pendingList) {
+ ev.attemptDecryption(this, {
+ isRetry: true
+ }).catch(_e => {
+ _logger.logger.info(`Still unable to decrypt event ${ev.getId()} after receiving key`);
+ });
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Outgoing requests
+ //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ async outgoingRequestLoop() {
+ if (this.outgoingRequestLoopRunning) {
+ return;
+ }
+ this.outgoingRequestLoopRunning = true;
+ try {
+ while (!this.stopped) {
+ const outgoingRequests = await this.olmMachine.outgoingRequests();
+ if (outgoingRequests.length == 0 || this.stopped) {
+ // no more messages to send (or we have been told to stop): exit the loop
+ return;
+ }
+ for (const msg of outgoingRequests) {
+ await this.outgoingRequestProcessor.makeOutgoingRequest(msg);
+ }
+ }
+ } catch (e) {
+ _logger.logger.error("Error processing outgoing-message requests from rust crypto-sdk", e);
+ } finally {
+ this.outgoingRequestLoopRunning = false;
+ }
+ }
+}
+exports.RustCrypto = RustCrypto;
+class EventDecryptor {
+ constructor(olmMachine) {
+ this.olmMachine = olmMachine;
+ /**
+ * Events which we couldn't decrypt due to unknown sessions / indexes.
+ *
+ * Map from senderKey to sessionId to Set of MatrixEvents
+ */
+ _defineProperty(this, "eventsPendingKey", new _utils.MapWithDefault(() => new _utils.MapWithDefault(() => new Set())));
+ }
+ async attemptEventDecryption(event) {
+ _logger.logger.info("Attempting decryption of event", event);
+ // add the event to the pending list *before* attempting to decrypt.
+ // then, if the key turns up while decryption is in progress (and
+ // decryption fails), we will schedule a retry.
+ // (fixes https://github.com/vector-im/element-web/issues/5001)
+ this.addEventToPendingList(event);
+ const res = await this.olmMachine.decryptRoomEvent(JSON.stringify({
+ event_id: event.getId(),
+ type: event.getWireType(),
+ sender: event.getSender(),
+ state_key: event.getStateKey(),
+ content: event.getWireContent(),
+ origin_server_ts: event.getTs()
+ }), new RustSdkCryptoJs.RoomId(event.getRoomId()));
+
+ // Success. We can remove the event from the pending list, if
+ // that hasn't already happened.
+ this.removeEventFromPendingList(event);
+ return {
+ clearEvent: JSON.parse(res.event),
+ claimedEd25519Key: res.senderClaimedEd25519Key,
+ senderCurve25519Key: res.senderCurve25519Key,
+ forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain
+ };
+ }
+
+ /**
+ * Look for events which are waiting for a given megolm session
+ *
+ * Returns a list of events which were encrypted by `session` and could not be decrypted
+ *
+ * @param session -
+ */
+ getEventsPendingRoomKey(session) {
+ const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64());
+ if (!senderPendingEvents) return [];
+ const sessionPendingEvents = senderPendingEvents.get(session.sessionId);
+ if (!sessionPendingEvents) return [];
+ const roomId = session.roomId.toString();
+ return [...sessionPendingEvents].filter(ev => ev.getRoomId() === roomId);
+ }
+
+ /**
+ * Add an event to the list of those awaiting their session keys.
+ */
+ addEventToPendingList(event) {
+ const content = event.getWireContent();
+ const senderKey = content.sender_key;
+ const sessionId = content.session_id;
+ const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey);
+ const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId);
+ sessionPendingEvents.add(event);
+ }
+
+ /**
+ * Remove an event from the list of those awaiting their session keys.
+ */
+ removeEventFromPendingList(event) {
+ const content = event.getWireContent();
+ const senderKey = content.sender_key;
+ const sessionId = content.session_id;
+ const senderPendingEvents = this.eventsPendingKey.get(senderKey);
+ if (!senderPendingEvents) return;
+ const sessionPendingEvents = senderPendingEvents.get(sessionId);
+ if (!sessionPendingEvents) return;
+ sessionPendingEvents.delete(event);
+
+ // also clean up the higher-level maps if they are now empty
+ if (sessionPendingEvents.size === 0) {
+ senderPendingEvents.delete(sessionId);
+ if (senderPendingEvents.size === 0) {
+ this.eventsPendingKey.delete(senderKey);
+ }
+ }
+ }
+} \ No newline at end of file