diff options
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous')
12 files changed, 831 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js new file mode 100644 index 0000000000..50da6ab883 --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js @@ -0,0 +1,240 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3906Rendezvous = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _ = require("."); +var _client = require("../client"); +var _feature = require("../feature"); +var _logger = require("../logger"); +var _utils = require("../utils"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* + Copyright 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. + */ +var PayloadType = /*#__PURE__*/function (PayloadType) { + PayloadType["Start"] = "m.login.start"; + PayloadType["Finish"] = "m.login.finish"; + PayloadType["Progress"] = "m.login.progress"; + return PayloadType; +}(PayloadType || {}); +var Outcome = /*#__PURE__*/function (Outcome) { + Outcome["Success"] = "success"; + Outcome["Failure"] = "failure"; + Outcome["Verified"] = "verified"; + Outcome["Declined"] = "declined"; + Outcome["Unsupported"] = "unsupported"; + return Outcome; +}(Outcome || {}); +const LOGIN_TOKEN_PROTOCOL = new _matrixEventsSdk.UnstableValue("login_token", "org.matrix.msc3906.login_token"); + +/** + * Implements MSC3906 to allow a user to sign in on a new device using QR code. + * This implementation only supports generating a QR code on a device that is already signed in. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3906Rendezvous { + /** + * @param channel - The secure channel used for communication + * @param client - The Matrix client in used on the device already logged in + * @param onFailure - Callback for when the rendezvous fails + */ + constructor(channel, client, onFailure) { + this.channel = channel; + this.client = client; + this.onFailure = onFailure; + _defineProperty(this, "newDeviceId", void 0); + _defineProperty(this, "newDeviceKey", void 0); + _defineProperty(this, "ourIntent", _.RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); + _defineProperty(this, "_code", void 0); + } + + /** + * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. + */ + get code() { + return this._code; + } + + /** + * Generate the code including doing partial set up of the channel where required. + */ + async generateCode() { + if (this._code) { + return; + } + this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + async startAfterShowingCode() { + const checksum = await this.channel.connect(); + _logger.logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); + + // in r1 of MSC3882 the availability is exposed as a capability + const capabilities = await this.client.getCapabilities(); + // in r0 of MSC3882 the availability is exposed as a feature flag + const features = await (0, _feature.buildFeatureSupportMap)(await this.client.getVersions()); + const capability = _client.UNSTABLE_MSC3882_CAPABILITY.findIn(capabilities); + + // determine available protocols + if (!capability?.enabled && features.get(_feature.Feature.LoginTokenRequest) === _feature.ServerSupport.Unsupported) { + _logger.logger.info("Server doesn't support MSC3882"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Unsupported + }); + await this.cancel(_.RendezvousFailureReason.HomeserverLacksSupport); + return undefined; + } + await this.send({ + type: PayloadType.Progress, + protocols: [LOGIN_TOKEN_PROTOCOL.name] + }); + _logger.logger.info("Waiting for other device to chose protocol"); + const { + type, + protocol, + outcome + } = await this.receive(); + if (type === PayloadType.Finish) { + // new device decided not to complete + switch (outcome ?? "") { + case "unsupported": + await this.cancel(_.RendezvousFailureReason.UnsupportedAlgorithm); + break; + default: + await this.cancel(_.RendezvousFailureReason.Unknown); + } + return undefined; + } + if (type !== PayloadType.Progress) { + await this.cancel(_.RendezvousFailureReason.Unknown); + return undefined; + } + if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) { + await this.cancel(_.RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + return checksum; + } + async receive() { + return await this.channel.receive(); + } + async send(payload) { + await this.channel.send(payload); + } + async declineLoginOnExistingDevice() { + _logger.logger.info("User declined sign in"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Declined + }); + } + async approveLoginOnExistingDevice(loginToken) { + // eslint-disable-next-line camelcase + await this.send({ + type: PayloadType.Progress, + login_token: loginToken, + homeserver: this.client.baseUrl + }); + _logger.logger.info("Waiting for outcome"); + const res = await this.receive(); + if (!res) { + return undefined; + } + const { + outcome, + device_id: deviceId, + device_key: deviceKey + } = res; + if (outcome !== "success") { + throw new Error("Linking failed"); + } + this.newDeviceId = deviceId; + this.newDeviceKey = deviceKey; + return deviceId; + } + async verifyAndCrossSignDevice(deviceInfo) { + if (!this.client.crypto) { + throw new Error("Crypto not available on client"); + } + if (!this.newDeviceId) { + throw new Error("No new device ID set"); + } + + // check that keys received from the server for the new device match those received from the device itself + if (deviceInfo.getFingerprint() !== this.newDeviceKey) { + throw new Error(`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`); + } + const userId = this.client.getUserId(); + if (!userId) { + throw new Error("No user ID set"); + } + // mark the device as verified locally + cross sign + _logger.logger.info(`Marking device ${this.newDeviceId} as verified`); + const info = await this.client.crypto.setDeviceVerification(userId, this.newDeviceId, true, false, true); + const masterPublicKey = this.client.crypto.crossSigningInfo.getId("master"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Verified, + verifying_device_id: this.client.getDeviceId(), + verifying_device_key: this.client.getDeviceEd25519Key(), + master_key: masterPublicKey + }); + return info; + } + + /** + * Verify the device and cross-sign it. + * @param timeout - time in milliseconds to wait for device to come online + * @returns the new device info if the device was verified + */ + async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000) { + if (!this.newDeviceId) { + throw new Error("No new device to sign"); + } + if (!this.newDeviceKey) { + _logger.logger.info("No new device key to sign"); + return undefined; + } + if (!this.client.crypto) { + throw new Error("Crypto not available on client"); + } + const userId = this.client.getUserId(); + if (!userId) { + throw new Error("No user ID set"); + } + let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); + if (!deviceInfo) { + _logger.logger.info("Going to wait for new device to be online"); + await (0, _utils.sleep)(timeout); + deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); + } + if (deviceInfo) { + return await this.verifyAndCrossSignDevice(deviceInfo); + } + throw new Error("Device not online within timeout"); + } + async cancel(reason) { + this.onFailure?.(reason); + await this.channel.cancel(reason); + } + async close() { + await this.channel.close(); + } +} +exports.MSC3906Rendezvous = MSC3906Rendezvous;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js new file mode 100644 index 0000000000..430afc16cd --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +});
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js new file mode 100644 index 0000000000..430afc16cd --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +});
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js new file mode 100644 index 0000000000..5190ebbb76 --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousError = 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. +*/ + +class RendezvousError extends Error { + constructor(message, code) { + super(message); + this.code = code; + } +} +exports.RendezvousError = RendezvousError;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js new file mode 100644 index 0000000000..ee6a9b987f --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js @@ -0,0 +1,36 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousFailureReason = 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. +*/ +let RendezvousFailureReason = /*#__PURE__*/function (RendezvousFailureReason) { + RendezvousFailureReason["UserDeclined"] = "user_declined"; + RendezvousFailureReason["OtherDeviceNotSignedIn"] = "other_device_not_signed_in"; + RendezvousFailureReason["OtherDeviceAlreadySignedIn"] = "other_device_already_signed_in"; + RendezvousFailureReason["Unknown"] = "unknown"; + RendezvousFailureReason["Expired"] = "expired"; + RendezvousFailureReason["UserCancelled"] = "user_cancelled"; + RendezvousFailureReason["InvalidCode"] = "invalid_code"; + RendezvousFailureReason["UnsupportedAlgorithm"] = "unsupported_algorithm"; + RendezvousFailureReason["DataMismatch"] = "data_mismatch"; + RendezvousFailureReason["UnsupportedTransport"] = "unsupported_transport"; + RendezvousFailureReason["HomeserverLacksSupport"] = "homeserver_lacks_support"; + return RendezvousFailureReason; +}({}); +exports.RendezvousFailureReason = RendezvousFailureReason;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js new file mode 100644 index 0000000000..5393ac57b9 --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js @@ -0,0 +1,27 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousIntent = 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. +*/ +let RendezvousIntent = /*#__PURE__*/function (RendezvousIntent) { + RendezvousIntent["LOGIN_ON_NEW_DEVICE"] = "login.start"; + RendezvousIntent["RECIPROCATE_LOGIN_ON_EXISTING_DEVICE"] = "login.reciprocate"; + return RendezvousIntent; +}({}); +exports.RendezvousIntent = RendezvousIntent;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js new file mode 100644 index 0000000000..430afc16cd --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +});
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js new file mode 100644 index 0000000000..3c9e5793bc --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js @@ -0,0 +1,194 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3903ECDHv2RendezvousChannel = void 0; +var _ = require(".."); +var _olmlib = require("../../crypto/olmlib"); +var _crypto = require("../../crypto/crypto"); +var _SASDecimal = require("../../crypto/verification/SASDecimal"); +var _NamespacedValue = require("../../NamespacedValue"); +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. + */ +const ECDH_V2 = new _NamespacedValue.UnstableValue("m.rendezvous.v2.curve25519-aes-sha256", "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256"); +async function importKey(key) { + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const imported = _crypto.subtleCrypto.importKey("raw", key, { + name: "AES-GCM" + }, false, ["encrypt", "decrypt"]); + return imported; +} + +/** + * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) + * X25519/ECDH key agreement based secure rendezvous channel. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3903ECDHv2RendezvousChannel { + constructor(transport, theirPublicKey, onFailure) { + this.transport = transport; + this.theirPublicKey = theirPublicKey; + this.onFailure = onFailure; + _defineProperty(this, "olmSAS", void 0); + _defineProperty(this, "ourPublicKey", void 0); + _defineProperty(this, "aesKey", void 0); + _defineProperty(this, "connected", false); + this.olmSAS = new global.Olm.SAS(); + this.ourPublicKey = (0, _olmlib.decodeBase64)(this.olmSAS.get_pubkey()); + } + async generateCode(intent) { + if (this.transport.ready) { + throw new Error("Code already generated"); + } + await this.transport.send({ + algorithm: ECDH_V2.name + }); + const rendezvous = { + rendezvous: { + algorithm: ECDH_V2.name, + key: (0, _olmlib.encodeUnpaddedBase64)(this.ourPublicKey), + transport: await this.transport.details() + }, + intent + }; + return rendezvous; + } + async connect() { + if (this.connected) { + throw new Error("Channel already connected"); + } + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + const isInitiator = !this.theirPublicKey; + if (isInitiator) { + // wait for the other side to send us their public key + const rawRes = await this.transport.receive(); + if (!rawRes) { + throw new Error("No response from other device"); + } + const res = rawRes; + const { + key, + algorithm + } = res; + if (!algorithm || !ECDH_V2.matches(algorithm) || !key) { + throw new _.RendezvousError("Unsupported algorithm: " + algorithm, _.RendezvousFailureReason.UnsupportedAlgorithm); + } + this.theirPublicKey = (0, _olmlib.decodeBase64)(key); + } else { + // send our public key unencrypted + await this.transport.send({ + algorithm: ECDH_V2.name, + key: (0, _olmlib.encodeUnpaddedBase64)(this.ourPublicKey) + }); + } + this.connected = true; + this.olmSAS.set_their_key((0, _olmlib.encodeUnpaddedBase64)(this.theirPublicKey)); + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; + const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + let aesInfo = ECDH_V2.name; + aesInfo += `|${(0, _olmlib.encodeUnpaddedBase64)(initiatorKey)}`; + aesInfo += `|${(0, _olmlib.encodeUnpaddedBase64)(recipientKey)}`; + const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); + this.aesKey = await importKey(aesKeyBytes); + + // blank the bytes out to make sure not kept in memory + aesKeyBytes.fill(0); + const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); + return (0, _SASDecimal.generateDecimalSas)(Array.from(rawChecksum)).join("-"); + } + async encrypt(data) { + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const iv = new Uint8Array(32); + _crypto.crypto.getRandomValues(iv); + const encodedData = new _crypto.TextEncoder().encode(JSON.stringify(data)); + const ciphertext = await _crypto.subtleCrypto.encrypt({ + name: "AES-GCM", + iv, + tagLength: 128 + }, this.aesKey, encodedData); + return { + iv: (0, _olmlib.encodeUnpaddedBase64)(iv), + ciphertext: (0, _olmlib.encodeUnpaddedBase64)(ciphertext) + }; + } + async send(payload) { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + return this.transport.send(await this.encrypt(payload)); + } + async decrypt({ + iv, + ciphertext + }) { + if (!ciphertext || !iv) { + throw new Error("Missing ciphertext and/or iv"); + } + const ciphertextBytes = (0, _olmlib.decodeBase64)(ciphertext); + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const plaintext = await _crypto.subtleCrypto.decrypt({ + name: "AES-GCM", + iv: (0, _olmlib.decodeBase64)(iv), + tagLength: 128 + }, this.aesKey, ciphertextBytes); + return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); + } + async receive() { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + const rawData = await this.transport.receive(); + if (!rawData) { + return undefined; + } + const data = rawData; + if (data.ciphertext && data.iv) { + return this.decrypt(data); + } + throw new Error("Data received but no ciphertext"); + } + async close() { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + async cancel(reason) { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} +exports.MSC3903ECDHv2RendezvousChannel = MSC3903ECDHv2RendezvousChannel;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js new file mode 100644 index 0000000000..e2d30513fd --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js @@ -0,0 +1,16 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3903ECDHv2RendezvousChannel = require("./MSC3903ECDHv2RendezvousChannel"); +Object.keys(_MSC3903ECDHv2RendezvousChannel).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3903ECDHv2RendezvousChannel[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3903ECDHv2RendezvousChannel[key]; + } + }); +});
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js new file mode 100644 index 0000000000..141c9da4fc --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js @@ -0,0 +1,82 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3906Rendezvous = require("./MSC3906Rendezvous"); +Object.keys(_MSC3906Rendezvous).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3906Rendezvous[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3906Rendezvous[key]; + } + }); +}); +var _RendezvousChannel = require("./RendezvousChannel"); +Object.keys(_RendezvousChannel).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousChannel[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousChannel[key]; + } + }); +}); +var _RendezvousCode = require("./RendezvousCode"); +Object.keys(_RendezvousCode).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousCode[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousCode[key]; + } + }); +}); +var _RendezvousError = require("./RendezvousError"); +Object.keys(_RendezvousError).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousError[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousError[key]; + } + }); +}); +var _RendezvousFailureReason = require("./RendezvousFailureReason"); +Object.keys(_RendezvousFailureReason).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousFailureReason[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousFailureReason[key]; + } + }); +}); +var _RendezvousIntent = require("./RendezvousIntent"); +Object.keys(_RendezvousIntent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousIntent[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousIntent[key]; + } + }); +}); +var _RendezvousTransport = require("./RendezvousTransport"); +Object.keys(_RendezvousTransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousTransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousTransport[key]; + } + }); +});
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js new file mode 100644 index 0000000000..6347229aca --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js @@ -0,0 +1,176 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3886SimpleHttpRendezvousTransport = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _logger = require("../../logger"); +var _utils = require("../../utils"); +var _ = require(".."); +var _httpApi = require("../../http-api"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* + Copyright 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. + */ +const TYPE = new _matrixEventsSdk.UnstableValue("http.v1", "org.matrix.msc3886.http.v1"); +/** + * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) + * simple HTTP rendezvous protocol. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3886SimpleHttpRendezvousTransport { + constructor({ + onFailure, + client, + fallbackRzServer, + fetchFn + }) { + _defineProperty(this, "uri", void 0); + _defineProperty(this, "etag", void 0); + _defineProperty(this, "expiresAt", void 0); + _defineProperty(this, "client", void 0); + _defineProperty(this, "fallbackRzServer", void 0); + _defineProperty(this, "fetchFn", void 0); + _defineProperty(this, "cancelled", false); + _defineProperty(this, "_ready", false); + _defineProperty(this, "onFailure", void 0); + this.fetchFn = fetchFn; + this.onFailure = onFailure; + this.client = client; + this.fallbackRzServer = fallbackRzServer; + } + get ready() { + return this._ready; + } + async details() { + if (!this.uri) { + throw new Error("Rendezvous not set up"); + } + return { + type: TYPE.name, + uri: this.uri + }; + } + fetch(resource, options) { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + async getPostEndpoint() { + try { + if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) { + return `${this.client.baseUrl}${_httpApi.ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; + } + } catch (err) { + _logger.logger.warn("Failed to get unstable features", err); + } + return this.fallbackRzServer; + } + async send(data) { + if (this.cancelled) { + return; + } + const method = this.uri ? "PUT" : "POST"; + const uri = this.uri ?? (await this.getPostEndpoint()); + if (!uri) { + throw new Error("Invalid rendezvous URI"); + } + const headers = { + "content-type": "application/json" + }; + if (this.etag) { + headers["if-match"] = this.etag; + } + const res = await this.fetch(uri, { + method, + headers, + body: JSON.stringify(data) + }); + if (res.status === 404) { + return this.cancel(_.RendezvousFailureReason.Unknown); + } + this.etag = res.headers.get("etag") ?? undefined; + if (method === "POST") { + const location = res.headers.get("location"); + if (!location) { + throw new Error("No rendezvous URI given"); + } + const expires = res.headers.get("expires"); + if (expires) { + this.expiresAt = new Date(expires); + } + // we would usually expect the final `url` to be set by a proper fetch implementation. + // however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback + const baseUrl = res.url ?? uri; + // resolve location header which could be relative or absolute + this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href; + this._ready = true; + } + } + async receive() { + if (!this.uri) { + throw new Error("Rendezvous not set up"); + } + // eslint-disable-next-line no-constant-condition + while (true) { + if (this.cancelled) { + return undefined; + } + const headers = {}; + if (this.etag) { + headers["if-none-match"] = this.etag; + } + const poll = await this.fetch(this.uri, { + method: "GET", + headers + }); + if (poll.status === 404) { + this.cancel(_.RendezvousFailureReason.Unknown); + return undefined; + } + + // rely on server expiring the channel rather than checking ourselves + + if (poll.headers.get("content-type") !== "application/json") { + this.etag = poll.headers.get("etag") ?? undefined; + } else if (poll.status === 200) { + this.etag = poll.headers.get("etag") ?? undefined; + return poll.json(); + } + await (0, _utils.sleep)(1000); + } + } + async cancel(reason) { + if (reason === _.RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) { + reason = _.RendezvousFailureReason.Expired; + } + this.cancelled = true; + this._ready = false; + this.onFailure?.(reason); + if (this.uri && reason === _.RendezvousFailureReason.UserDeclined) { + try { + await this.fetch(this.uri, { + method: "DELETE" + }); + } catch (e) { + _logger.logger.warn(e); + } + } + } +} +exports.MSC3886SimpleHttpRendezvousTransport = MSC3886SimpleHttpRendezvousTransport;
\ No newline at end of file diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js new file mode 100644 index 0000000000..a1a4c56c64 --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js @@ -0,0 +1,16 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3886SimpleHttpRendezvousTransport = require("./MSC3886SimpleHttpRendezvousTransport"); +Object.keys(_MSC3886SimpleHttpRendezvousTransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3886SimpleHttpRendezvousTransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3886SimpleHttpRendezvousTransport[key]; + } + }); +});
\ No newline at end of file |