diff options
author | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
---|---|---|
committer | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
commit | 633c92eae865e957121e08de634aeee11a8b3992 (patch) | |
tree | 09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts | |
parent | c4657e4509733699c0f26a3c900bab47e915d5a0 (diff) | |
download | pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2 pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip |
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts | 264 |
1 files changed, 264 insertions, 0 deletions
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<MSC3906RendezvousPayload>, + 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<void> { + if (this._code) { + return; + } + + this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + + public async startAfterShowingCode(): Promise<string | undefined> { + 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<MSC3906RendezvousPayload> { + return (await this.channel.receive()) as MSC3906RendezvousPayload; + } + + private async send(payload: MSC3906RendezvousPayload): Promise<void> { + await this.channel.send(payload); + } + + public async declineLoginOnExistingDevice(): Promise<void> { + logger.info("User declined sign in"); + await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined }); + } + + public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> { + // 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<CrossSigningInfo | 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.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<DeviceInfo | CrossSigningInfo | undefined> { + 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<void> { + this.onFailure?.(reason); + await this.channel.cancel(reason); + } + + public async close(): Promise<void> { + await this.channel.close(); + } +} |