From 633c92eae865e957121e08de634aeee11a8b3992 Mon Sep 17 00:00:00 2001 From: RaindropsSys Date: Mon, 24 Apr 2023 14:03:36 +0200 Subject: Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated) --- .../src/rendezvous/MSC3906Rendezvous.ts | 264 +++++++++++++++++++++ .../src/rendezvous/RendezvousChannel.ts | 48 ++++ .../matrix-js-sdk/src/rendezvous/RendezvousCode.ts | 25 ++ .../src/rendezvous/RendezvousError.ts | 23 ++ .../src/rendezvous/RendezvousFailureReason.ts | 31 +++ .../src/rendezvous/RendezvousIntent.ts | 20 ++ .../src/rendezvous/RendezvousTransport.ts | 58 +++++ .../channels/MSC3903ECDHv2RendezvousChannel.ts | 259 ++++++++++++++++++++ .../matrix-js-sdk/src/rendezvous/channels/index.ts | 17 ++ .../matrix-js-sdk/src/rendezvous/index.ts | 23 ++ .../MSC3886SimpleHttpRendezvousTransport.ts | 193 +++++++++++++++ .../src/rendezvous/transports/index.ts | 17 ++ 12 files changed, 978 insertions(+) create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous') diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts new file mode 100644 index 0000000..f431c83 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts @@ -0,0 +1,264 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { UnstableValue } from "matrix-events-sdk"; + +import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from "."; +import { MatrixClient } from "../client"; +import { CrossSigningInfo } from "../crypto/CrossSigning"; +import { DeviceInfo } from "../crypto/deviceinfo"; +import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature"; +import { logger } from "../logger"; +import { sleep } from "../utils"; + +enum PayloadType { + Start = "m.login.start", + Finish = "m.login.finish", + Progress = "m.login.progress", +} + +enum Outcome { + Success = "success", + Failure = "failure", + Verified = "verified", + Declined = "declined", + Unsupported = "unsupported", +} + +export interface MSC3906RendezvousPayload { + type: PayloadType; + intent?: RendezvousIntent; + outcome?: Outcome; + device_id?: string; + device_key?: string; + verifying_device_id?: string; + verifying_device_key?: string; + master_key?: string; + protocols?: string[]; + protocol?: string; + login_token?: string; + homeserver?: string; +} + +const LOGIN_TOKEN_PROTOCOL = new 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. + */ +export class MSC3906Rendezvous { + private newDeviceId?: string; + private newDeviceKey?: string; + private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; + private _code?: string; + + /** + * @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 + */ + public constructor( + private channel: RendezvousChannel, + private client: MatrixClient, + public onFailure?: RendezvousFailureListener, + ) {} + + /** + * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. + */ + public get code(): string | undefined { + return this._code; + } + + /** + * Generate the code including doing partial set up of the channel where required. + */ + public async generateCode(): Promise { + if (this._code) { + return; + } + + this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + + public async startAfterShowingCode(): Promise { + const checksum = await this.channel.connect(); + + logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); + + const features = await buildFeatureSupportMap(await this.client.getVersions()); + // determine available protocols + if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { + 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.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; + } + + private async receive(): Promise { + return (await this.channel.receive()) as MSC3906RendezvousPayload; + } + + private async send(payload: MSC3906RendezvousPayload): Promise { + await this.channel.send(payload); + } + + public async declineLoginOnExistingDevice(): Promise { + logger.info("User declined sign in"); + await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined }); + } + + public async approveLoginOnExistingDevice(loginToken: string): Promise { + // eslint-disable-next-line camelcase + await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); + + 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; + } + + private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo): Promise { + 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.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 + */ + public async verifyNewDeviceOnExistingDevice( + timeout = 10 * 1000, + ): Promise { + if (!this.newDeviceId) { + throw new Error("No new device to sign"); + } + + if (!this.newDeviceKey) { + 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.info("Going to wait for new device to be online"); + await 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"); + } + + public async cancel(reason: RendezvousFailureReason): Promise { + this.onFailure?.(reason); + await this.channel.cancel(reason); + } + + public async close(): Promise { + await this.channel.close(); + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts new file mode 100644 index 0000000..549ebc8 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts @@ -0,0 +1,48 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RendezvousCode, RendezvousIntent, RendezvousFailureReason } from "."; + +export interface RendezvousChannel { + /** + * @returns the checksum/confirmation digits to be shown to the user + */ + connect(): Promise; + + /** + * Send a payload via the channel. + * @param data - payload to send + */ + send(data: T): Promise; + + /** + * Receive a payload from the channel. + * @returns the received payload + */ + receive(): Promise | undefined>; + + /** + * Close the channel and clear up any resources. + */ + close(): Promise; + + /** + * @returns a representation of the channel that can be encoded in a QR or similar + */ + generateCode(intent: RendezvousIntent): Promise; + + cancel(reason: RendezvousFailureReason): Promise; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts new file mode 100644 index 0000000..86608aa --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts @@ -0,0 +1,25 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RendezvousTransportDetails, RendezvousIntent } from "."; + +export interface RendezvousCode { + intent: RendezvousIntent; + rendezvous?: { + transport: RendezvousTransportDetails; + algorithm: string; + }; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts new file mode 100644 index 0000000..8b76fc1 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts @@ -0,0 +1,23 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RendezvousFailureReason } from "."; + +export class RendezvousError extends Error { + public constructor(message: string, public readonly code: RendezvousFailureReason) { + super(message); + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts new file mode 100644 index 0000000..b19a91c --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts @@ -0,0 +1,31 @@ +/* +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. +*/ + +export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void; + +export enum RendezvousFailureReason { + UserDeclined = "user_declined", + OtherDeviceNotSignedIn = "other_device_not_signed_in", + OtherDeviceAlreadySignedIn = "other_device_already_signed_in", + Unknown = "unknown", + Expired = "expired", + UserCancelled = "user_cancelled", + InvalidCode = "invalid_code", + UnsupportedAlgorithm = "unsupported_algorithm", + DataMismatch = "data_mismatch", + UnsupportedTransport = "unsupported_transport", + HomeserverLacksSupport = "homeserver_lacks_support", +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts new file mode 100644 index 0000000..db53ef9 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts @@ -0,0 +1,20 @@ +/* +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. +*/ + +export enum RendezvousIntent { + LOGIN_ON_NEW_DEVICE = "login.start", + RECIPROCATE_LOGIN_ON_EXISTING_DEVICE = "login.reciprocate", +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts new file mode 100644 index 0000000..08905be --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts @@ -0,0 +1,58 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RendezvousFailureListener, RendezvousFailureReason } from "."; + +export interface RendezvousTransportDetails { + type: string; +} + +/** + * Interface representing a generic rendezvous transport. + */ +export interface RendezvousTransport { + /** + * Ready state of the transport. This is set to true when the transport is ready to be used. + */ + readonly ready: boolean; + + /** + * Listener for cancellation events. This is called when the rendezvous is cancelled or fails. + */ + onFailure?: RendezvousFailureListener; + + /** + * @returns the transport details that can be encoded in a QR or similar + */ + details(): Promise; + + /** + * Send data via the transport. + * @param data - the data itself + */ + send(data: T): Promise; + + /** + * Receive data from the transport. + */ + receive(): Promise | undefined>; + + /** + * Cancel the rendezvous. This will call `onCancelled()` if it is set. + * @param reason - the reason for the cancellation/failure + */ + cancel(reason: RendezvousFailureReason): Promise; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts new file mode 100644 index 0000000..be60ee5 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts @@ -0,0 +1,259 @@ +/* +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. +*/ + +import { SAS } from "@matrix-org/olm"; + +import { + RendezvousError, + RendezvousCode, + RendezvousIntent, + RendezvousChannel, + RendezvousTransportDetails, + RendezvousTransport, + RendezvousFailureReason, +} from ".."; +import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib"; +import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto"; +import { generateDecimalSas } from "../../crypto/verification/SASDecimal"; +import { UnstableValue } from "../../NamespacedValue"; + +const ECDH_V2 = new UnstableValue( + "m.rendezvous.v2.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256", +); + +export interface ECDHv2RendezvousCode extends RendezvousCode { + rendezvous: { + transport: RendezvousTransportDetails; + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName; + key: string; + }; +} + +export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; + +export interface PlainTextPayload { + algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName; + key?: string; +} + +export interface EncryptedPayload { + iv: string; + ciphertext: string; +} + +async function importKey(key: Uint8Array): Promise { + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const imported = 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. + */ +export class MSC3903ECDHv2RendezvousChannel implements RendezvousChannel { + private olmSAS?: SAS; + private ourPublicKey: Uint8Array; + private aesKey?: CryptoKey; + private connected = false; + + public constructor( + private transport: RendezvousTransport, + private theirPublicKey?: Uint8Array, + public onFailure?: (reason: RendezvousFailureReason) => void, + ) { + this.olmSAS = new global.Olm.SAS(); + this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); + } + + public async generateCode(intent: RendezvousIntent): Promise { + if (this.transport.ready) { + throw new Error("Code already generated"); + } + + await this.transport.send({ algorithm: ECDH_V2.name }); + + const rendezvous: ECDHv2RendezvousCode = { + rendezvous: { + algorithm: ECDH_V2.name, + key: encodeUnpaddedBase64(this.ourPublicKey), + transport: await this.transport.details(), + }, + intent, + }; + + return rendezvous; + } + + public async connect(): Promise { + 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 as Partial; + const { key, algorithm } = res; + if (!algorithm || !ECDH_V2.matches(algorithm) || !key) { + throw new RendezvousError( + "Unsupported algorithm: " + algorithm, + RendezvousFailureReason.UnsupportedAlgorithm, + ); + } + + this.theirPublicKey = decodeBase64(key); + } else { + // send our public key unencrypted + await this.transport.send({ + algorithm: ECDH_V2.name, + key: encodeUnpaddedBase64(this.ourPublicKey), + }); + } + + this.connected = true; + + this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!)); + + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; + const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; + let aesInfo = ECDH_V2.name; + aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; + aesInfo += `|${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 generateDecimalSas(Array.from(rawChecksum)).join("-"); + } + + private async encrypt(data: T): Promise { + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const iv = new Uint8Array(32); + crypto.getRandomValues(iv); + + const encodedData = new TextEncoder().encode(JSON.stringify(data)); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-GCM", + iv, + tagLength: 128, + }, + this.aesKey as CryptoKey, + encodedData, + ); + + return { + iv: encodeUnpaddedBase64(iv), + ciphertext: encodeUnpaddedBase64(ciphertext), + }; + } + + public async send(payload: T): Promise { + 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)); + } + + private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise> { + if (!ciphertext || !iv) { + throw new Error("Missing ciphertext and/or iv"); + } + + const ciphertextBytes = decodeBase64(ciphertext); + + if (!subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-GCM", + iv: decodeBase64(iv), + tagLength: 128, + }, + this.aesKey as CryptoKey, + ciphertextBytes, + ); + + return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); + } + + public async receive(): Promise | undefined> { + 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 as Partial; + if (data.ciphertext && data.iv) { + return this.decrypt(data as EncryptedPayload); + } + + throw new Error("Data received but no ciphertext"); + } + + public async close(): Promise { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + + public async cancel(reason: RendezvousFailureReason): Promise { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts new file mode 100644 index 0000000..f157bbe --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts @@ -0,0 +1,17 @@ +/* +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. +*/ + +export * from "./MSC3903ECDHv2RendezvousChannel"; diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts new file mode 100644 index 0000000..379b133 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts @@ -0,0 +1,23 @@ +/* +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. +*/ + +export * from "./MSC3906Rendezvous"; +export * from "./RendezvousChannel"; +export * from "./RendezvousCode"; +export * from "./RendezvousError"; +export * from "./RendezvousFailureReason"; +export * from "./RendezvousIntent"; +export * from "./RendezvousTransport"; diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts new file mode 100644 index 0000000..430ee92 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -0,0 +1,193 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { UnstableValue } from "matrix-events-sdk"; + +import { logger } from "../../logger"; +import { sleep } from "../../utils"; +import { + RendezvousFailureListener, + RendezvousFailureReason, + RendezvousTransport, + RendezvousTransportDetails, +} from ".."; +import { MatrixClient } from "../../matrix"; +import { ClientPrefix } from "../../http-api"; + +const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1"); + +export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { + uri: string; +} + +/** + * 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. + */ +export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { + private uri?: string; + private etag?: string; + private expiresAt?: Date; + private client: MatrixClient; + private fallbackRzServer?: string; + private fetchFn?: typeof global.fetch; + private cancelled = false; + private _ready = false; + public onFailure?: RendezvousFailureListener; + + public constructor({ + onFailure, + client, + fallbackRzServer, + fetchFn, + }: { + fetchFn?: typeof global.fetch; + onFailure?: RendezvousFailureListener; + client: MatrixClient; + fallbackRzServer?: string; + }) { + this.fetchFn = fetchFn; + this.onFailure = onFailure; + this.client = client; + this.fallbackRzServer = fallbackRzServer; + } + + public get ready(): boolean { + return this._ready; + } + + public async details(): Promise { + if (!this.uri) { + throw new Error("Rendezvous not set up"); + } + + return { + type: TYPE.name, + uri: this.uri, + }; + } + + private fetch(resource: URL | string, options?: RequestInit): ReturnType { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + private async getPostEndpoint(): Promise { + try { + if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) { + return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; + } + } catch (err) { + logger.warn("Failed to get unstable features", err); + } + + return this.fallbackRzServer; + } + + public async send(data: T): Promise { + 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: Record = { "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; + } + } + + public async receive(): Promise | undefined> { + 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: Record = {}; + 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 sleep(1000); + } + } + + public async cancel(reason: RendezvousFailureReason): Promise { + 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.warn(e); + } + } + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts new file mode 100644 index 0000000..6d8d642 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts @@ -0,0 +1,17 @@ +/* +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. +*/ + +export * from "./MSC3886SimpleHttpRendezvousTransport"; -- cgit