diff options
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto')
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 |