From 9e48662de191eb274a8b0c8537324500d1eafe5e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 10:17:59 +0100 Subject: [PATCH 01/74] Clean implementation of MSC3886 and MSC3903 --- src/rendezvous/cancellationReason.ts | 31 ++ src/rendezvous/channel.ts | 54 ++++ src/rendezvous/channels/ecdhV1.ts | 276 ++++++++++++++++++ src/rendezvous/channels/index.ts | 21 ++ src/rendezvous/code.ts | 31 ++ src/rendezvous/error.ts | 23 ++ src/rendezvous/index.ts | 84 ++++++ src/rendezvous/transport.ts | 59 ++++ src/rendezvous/transports/index.ts | 17 ++ .../transports/simpleHttpTransport.ts | 193 ++++++++++++ 10 files changed, 789 insertions(+) create mode 100644 src/rendezvous/cancellationReason.ts create mode 100644 src/rendezvous/channel.ts create mode 100644 src/rendezvous/channels/ecdhV1.ts create mode 100644 src/rendezvous/channels/index.ts create mode 100644 src/rendezvous/code.ts create mode 100644 src/rendezvous/error.ts create mode 100644 src/rendezvous/index.ts create mode 100644 src/rendezvous/transport.ts create mode 100644 src/rendezvous/transports/index.ts create mode 100644 src/rendezvous/transports/simpleHttpTransport.ts diff --git a/src/rendezvous/cancellationReason.ts b/src/rendezvous/cancellationReason.ts new file mode 100644 index 00000000000..c23168dd154 --- /dev/null +++ b/src/rendezvous/cancellationReason.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/src/rendezvous/channel.ts b/src/rendezvous/channel.ts new file mode 100644 index 00000000000..256016af921 --- /dev/null +++ b/src/rendezvous/channel.ts @@ -0,0 +1,54 @@ +/* +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, + RendezvousTransport, + RendezvousIntent, + RendezvousFailureReason, +} from "."; + +export interface RendezvousChannel { + transport: RendezvousTransport; + /** + * @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: any): Promise; + + /** + * Receive a payload from the channel. + * @returns the received payload + */ + receive(): Promise; + + /** + * 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/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts new file mode 100644 index 00000000000..99bcefded56 --- /dev/null +++ b/src/rendezvous/channels/ecdhV1.ts @@ -0,0 +1,276 @@ +/* +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 { SAS } from '@matrix-org/olm'; + +import { logger } from '../../logger'; +import { RendezvousError } from '../error'; +import { + RendezvousCode, + RendezvousIntent, + RendezvousChannel, + RendezvousTransportDetails, + RendezvousTransport, + RendezvousFailureReason, +} from '../index'; +import { SecureRendezvousChannelAlgorithm } from '.'; +import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; + +const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? + (window.crypto.subtle || window.crypto.webkitSubtle) : null; + +export interface ECDHv1RendezvousCode extends RendezvousCode { + rendezvous: { + transport: RendezvousTransportDetails; + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1; + key: string; + }; +} + +// The underlying algorithm is the same as: +// https://github.com/matrix-org/matrix-js-sdk/blob/75204d5cd04d67be100fca399f83b1a66ffb8118/src/crypto/verification/SAS.ts#L54-L68 +function generateDecimalSas(sasBytes: number[]): string { + /** + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + const digits = [ + (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, + ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, + ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000, + ]; + + return digits.join('-'); +} + +async function importKey(key: Uint8Array): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto 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. + */ +export class ECDHv1RendezvousChannel implements RendezvousChannel { + private olmSAS?: SAS; + private ourPublicKey: Uint8Array; + private aesKey?: CryptoKey; + public onFailure?: (reason: RendezvousFailureReason) => void; + + constructor( + public transport: RendezvousTransport, + private theirPublicKey?: Uint8Array, + ) { + 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'); + } + + const data = { + "algorithm": SecureRendezvousChannelAlgorithm.ECDH_V1, + }; + + await this.send({ algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1 }); + + const rendezvous: ECDHv1RendezvousCode = { + "rendezvous": { + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + key: encodeBase64(this.ourPublicKey), + transport: await this.transport.details(), + ...data, + }, + intent, + }; + + return rendezvous; + } + + async connect(): Promise { + 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 + logger.info('Waiting for other device to send their public key'); + const res = await this.receive(); + if (!res) { + throw new Error('No response from other device'); + } + const { key, algorithm } = res; + + if (algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1 || (isInitiator && !key)) { + throw new RendezvousError( + 'Unsupported algorithm: ' + algorithm, + RendezvousFailureReason.UnsupportedAlgorithm, + ); + } + + this.theirPublicKey = decodeBase64(key); + } else { + // send our public key unencrypted + await this.send({ + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + key: encodeBase64(this.ourPublicKey), + }); + } + + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey)); + + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; + const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + let aesInfo = SecureRendezvousChannelAlgorithm.ECDH_V1.toString(); + aesInfo += `|${encodeBase64(initiatorKey)}`; + aesInfo += `|${encodeBase64(recipientKey)}`; + + const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); + + this.aesKey = await importKey(aesKeyBytes); + + logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); + logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey)}`); + logger.debug(`AES info: ${aesInfo}`); + logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); + + const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); + return generateDecimalSas(Array.from(rawChecksum)); + } + + private async encrypt(data: any): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto not available'); + } + + const iv = new Uint8Array(32); + window.crypto.getRandomValues(iv); + + const encodedData = new TextEncoder().encode(data); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-GCM", + iv, + }, + this.aesKey, + encodedData, + ); + + return JSON.stringify({ + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + }); + } + + public async send(data: any) { + if (!this.olmSAS) { + throw new Error('Channel closed'); + } + + const stringifiedData = JSON.stringify(data); + + if (this.aesKey) { + logger.info(`Encrypting: ${stringifiedData}`); + await this.transport.send('application/json', await this.encrypt(stringifiedData)); + } else { + await this.transport.send('application/json', stringifiedData); + } + } + + private async decrypt({ iv, ciphertext }: { iv: string, ciphertext: string }): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto not available'); + } + + if (!ciphertext || !iv) { + throw new Error('Missing ciphertext and/or iv'); + } + + const ciphertextBytes = decodeBase64(ciphertext); + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-GCM", + iv: decodeBase64(iv), + }, + this.aesKey, + ciphertextBytes, + ); + + return new TextDecoder().decode(new Uint8Array(plaintext)); + } + + public async receive(): Promise { + if (!this.olmSAS) { + throw new Error('Channel closed'); + } + + const data = await this.transport.receive(); + logger.info(`Received data: ${JSON.stringify(data)}`); + if (!data) { + return data; + } + + if (data.ciphertext) { + if (!this.aesKey) { + throw new Error('Shared secret not set up'); + } + const decrypted = await this.decrypt(data); + logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); + return decrypted; + } else if (this.aesKey) { + throw new Error('Data received but no ciphertext'); + } + + return data; + } + + public async close() { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + + public async cancel(reason: RendezvousFailureReason) { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts new file mode 100644 index 00000000000..059223029dc --- /dev/null +++ b/src/rendezvous/channels/index.ts @@ -0,0 +1,21 @@ +/* +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 './ecdhV1'; + +export enum SecureRendezvousChannelAlgorithm { + ECDH_V1 = "m.rendezvous.v1.curve25519-aes-sha256" +} diff --git a/src/rendezvous/code.ts b/src/rendezvous/code.ts new file mode 100644 index 00000000000..c77379ba6ac --- /dev/null +++ b/src/rendezvous/code.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. +*/ + +import { SecureRendezvousChannelAlgorithm } from "./channels"; +import { RendezvousTransportDetails } from "./transport"; + +export enum RendezvousIntent { + LOGIN_ON_NEW_DEVICE = "login.start", + RECIPROCATE_LOGIN_ON_EXISTING_DEVICE = "login.reciprocate", +} + +export interface RendezvousCode { + intent: RendezvousIntent; + rendezvous?: { + transport: RendezvousTransportDetails; + algorithm: SecureRendezvousChannelAlgorithm; + }; +} diff --git a/src/rendezvous/error.ts b/src/rendezvous/error.ts new file mode 100644 index 00000000000..50a2bd3e4b0 --- /dev/null +++ b/src/rendezvous/error.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 "./cancellationReason"; + +export class RendezvousError extends Error { + constructor(message: string, public readonly code: RendezvousFailureReason) { + super(message); + } +} diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts new file mode 100644 index 00000000000..c3113d22c0c --- /dev/null +++ b/src/rendezvous/index.ts @@ -0,0 +1,84 @@ +/* +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 './cancellationReason'; +import { RendezvousChannel } from './channel'; +import { RendezvousCode, RendezvousIntent } from './code'; +import { RendezvousError } from './error'; +import { SimpleHttpRendezvousTransport, SimpleHttpRendezvousTransportDetails } from './transports'; +import { decodeBase64 } from '../crypto/olmlib'; +import { ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; +import { logger } from '../logger'; + +export * from './code'; +export * from './cancellationReason'; +export * from './transport'; +export * from './channel'; + +/** + * Attempts to parse the given code as a rendezvous and return a channel and transport. + * @param code The code to parse. + * @param onCancelled the cancellation listener to use for the transport and secure channel. + * @returns The channel and intent of the generatoer + */ +export async function buildChannelFromCode( + code: string, + onCancelled: RendezvousFailureListener, +): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { + let parsed: RendezvousCode; + try { + parsed = JSON.parse(code) as RendezvousCode; + } catch (err) { + throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); + } + + const { intent, rendezvous } = parsed; + + if (rendezvous?.transport.type !== 'http.v1') { + throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedTransport); + } + + const transportDetails = rendezvous.transport as SimpleHttpRendezvousTransportDetails; + + if (typeof transportDetails.uri !== 'string') { + throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); + } + + if (!intent || !Object.values(RendezvousIntent).includes(intent)) { + throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); + } + + const transport = new SimpleHttpRendezvousTransport( + onCancelled, + undefined, // client + undefined, // hsUrl + undefined, // fallbackRzServer + transportDetails.uri); + + if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { + throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); + } + + const ecdhCode = parsed as ECDHv1RendezvousCode; + + const theirPublicKey = decodeBase64(ecdhCode.rendezvous.key); + + logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); + return { + channel: new ECDHv1RendezvousChannel(transport, theirPublicKey), + intent, + }; +} diff --git a/src/rendezvous/transport.ts b/src/rendezvous/transport.ts new file mode 100644 index 00000000000..a28131dc372 --- /dev/null +++ b/src/rendezvous/transport.ts @@ -0,0 +1,59 @@ +/* +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 "./cancellationReason"; + +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. + */ + 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 contentType the content type of the data being sent + * @param data the data itself + */ + send(contentType: string, data: any): Promise; + + /** + * Receive data from the transport. + */ + receive(): Promise; + + /** + * 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/src/rendezvous/transports/index.ts b/src/rendezvous/transports/index.ts new file mode 100644 index 00000000000..8e7cadab1bc --- /dev/null +++ b/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 './simpleHttpTransport'; diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts new file mode 100644 index 00000000000..9d1ec565c10 --- /dev/null +++ b/src/rendezvous/transports/simpleHttpTransport.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 { logger } from '../../logger'; +import { sleep } from '../../utils'; +import { RendezvousFailureListener, RendezvousFailureReason } from '../cancellationReason'; +import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; +import { MatrixClient } from '../../matrix'; +import { PREFIX_UNSTABLE } from '../../http-api'; + +export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { + type: 'http.v1'; + uri: string; +} + +/** + * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) + * simple HTTP rendezvous protocol. + */ +export class SimpleHttpRendezvousTransport implements RendezvousTransport { + ready = false; + cancelled = false; + private uri?: string; + private etag?: string; + private expiresAt?: Date; + + private static fetch(resource: URL | string, options?: RequestInit): ReturnType { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + private static fetchFn?: typeof global.fetch; + + public static setFetchFn(fetchFn: typeof global.fetch): void { + SimpleHttpRendezvousTransport.fetchFn = fetchFn; + } + + constructor( + public onFailure?: RendezvousFailureListener, + private client?: MatrixClient, + private hsUrl?: string, + private fallbackRzServer?: string, + rendezvousUri?: string, + ) { + this.uri = rendezvousUri; + this.ready = !!this.uri; + } + + async details(): Promise { + if (!this.uri) { + throw new Error('Rendezvous not set up'); + } + + return { + type: 'http.v1', + uri: this.uri, + }; + } + + private async getPostEndpoint(): Promise { + if (!this.client && this.hsUrl) { + this.client = new MatrixClient({ + baseUrl: this.hsUrl, + }); + } + + if (this.client) { + try { + if (await this.client.doesServerSupportUnstableFeature('org.matrix.msc3886')) { + return `${this.client.baseUrl}${PREFIX_UNSTABLE}/org.matrix.msc3886/rendezvous`; + } + } catch (err) { + logger.warn('Failed to get unstable features', err); + } + } + + return this.fallbackRzServer; + } + + async send(contentType: string, data: any) { + 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'); + } + + logger.debug(`Sending data: ${data} to ${uri}`); + + const headers: Record = { 'content-type': contentType }; + if (this.etag) { + headers['if-match'] = this.etag; + } + + const res = await SimpleHttpRendezvousTransport.fetch(uri, { method, + headers, + body: data, + }); + if (res.status === 404) { + return this.cancel(RendezvousFailureReason.Unknown); + } + this.etag = res.headers.get("etag") ?? undefined; + + logger.debug(`Posted data to ${uri} new etag ${this.etag}`); + + 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); + } + // resolve location header which could be relative or absolute + this.uri = new URL(location, `${res.url}${res.url.endsWith('/') ? '' : '/'}`).href; + this.ready =true; + } + } + + async receive(): Promise { + if (!this.uri) { + throw new Error('Rendezvous not set up'); + } + // eslint-disable-next-line no-constant-condition + while (true) { + if (this.cancelled) { + return; + } + logger.debug(`Polling: ${this.uri} after etag ${this.etag}`); + const headers: Record = {}; + if (this.etag) { + headers['if-none-match'] = this.etag; + } + const poll = await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "GET", headers }); + + logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); + if (poll.status === 404) { + return this.cancel(RendezvousFailureReason.Unknown); + } + + // 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; + const data = await poll.json(); + logger.debug(`Received data: ${JSON.stringify(data)} from ${this.uri} with etag ${this.etag}`); + return data; + } + await sleep(1000); + } + } + + async cancel(reason: RendezvousFailureReason) { + 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 { + logger.debug(`Deleting channel: ${this.uri}`); + await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "DELETE" }); + } catch (e) { + logger.warn(e); + } + } + } +} From 9513b29c3eaab2ac0b0c95e3cb527468ae9cd4f4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 13:22:32 +0100 Subject: [PATCH 02/74] Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better --- src/rendezvous/index.ts | 9 ++--- .../transports/simpleHttpTransport.ts | 33 ++++++++++++++----- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index c3113d22c0c..2a741243ac5 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -36,7 +36,7 @@ export * from './channel'; */ export async function buildChannelFromCode( code: string, - onCancelled: RendezvousFailureListener, + onFailure: RendezvousFailureListener, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -61,12 +61,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport( - onCancelled, - undefined, // client - undefined, // hsUrl - undefined, // fallbackRzServer - transportDetails.uri); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 9d1ec565c10..14cd9e5e44e 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -36,6 +36,10 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private uri?: string; private etag?: string; private expiresAt?: Date; + public onFailure?: RendezvousFailureListener; + private client?: MatrixClient; + private hsUrl?: string; + private fallbackRzServer?: string; private static fetch(resource: URL | string, options?: RequestInit): ReturnType { if (this.fetchFn) { @@ -50,13 +54,23 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { SimpleHttpRendezvousTransport.fetchFn = fetchFn; } - constructor( - public onFailure?: RendezvousFailureListener, - private client?: MatrixClient, - private hsUrl?: string, - private fallbackRzServer?: string, - rendezvousUri?: string, - ) { + constructor({ + onFailure, + client, + hsUrl, + fallbackRzServer, + rendezvousUri, + }: { + onFailure?: RendezvousFailureListener; + client?: MatrixClient; + hsUrl?: string; + fallbackRzServer?: string; + rendezvousUri?: string; + }) { + this.onFailure = onFailure; + this.client = client; + this.hsUrl = hsUrl; + this.fallbackRzServer = fallbackRzServer; this.uri = rendezvousUri; this.ready = !!this.uri; } @@ -130,8 +144,11 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { 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, `${res.url}${res.url.endsWith('/') ? '' : '/'}`).href; + this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}`).href; this.ready =true; } } From d167030a50347eb0ba77197930fe9514cc51d5d3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 13:22:49 +0100 Subject: [PATCH 03/74] Start of some unit tests --- spec/unit/rendezvous/rendezvous.spec.ts | 128 ++++++++ .../rendezvous/simpleHttpTransport.spec.ts | 287 ++++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100644 spec/unit/rendezvous/rendezvous.spec.ts create mode 100644 spec/unit/rendezvous/simpleHttpTransport.spec.ts diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts new file mode 100644 index 00000000000..d948731d482 --- /dev/null +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -0,0 +1,128 @@ +/* +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 MockHttpBackend from "matrix-mock-request"; + +import '../../olm-loader'; +import { buildChannelFromCode } from "../../../src/rendezvous"; +import { SimpleHttpRendezvousTransport } from '../../../src/rendezvous/transports'; + +describe("Rendezvous", function() { + beforeAll(async function() { + await global.Olm.init(); + }); + + const getHttpBackend = (): MockHttpBackend => { + const httpBackend = new MockHttpBackend(); + SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); + return httpBackend; + }; + + describe("buildChannelFromCode", function() { + it("non-JSON", function() { + expect(buildChannelFromCode("xyz", () => {})).rejects.toThrow("Invalid code"); + }); + + it("invalid JSON", function() { + expect(buildChannelFromCode(JSON.stringify({}), () => {})).rejects.toThrow("Unsupported transport"); + }); + + it("invalid transport type", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "foo" } }, + }), () => {})).rejects.toThrow("Unsupported transport"); + }); + + it("missing URI", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1" } }, + }), () => {})).rejects.toThrow("Invalid code"); + }); + + it("invalid URI field", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1", uri: false } }, + }), () => {})).rejects.toThrow("Invalid code"); + }); + + it("missing intent", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1", uri: "something" } }, + }), () => {})).rejects.toThrow("Invalid intent"); + }); + + it("invalid intent", function() { + expect(buildChannelFromCode(JSON.stringify({ + intent: 'asd', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {})).rejects.toThrow("Invalid intent"); + }); + + it("login.reciprocate", async function() { + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.reciprocate', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {}); + expect(x.intent).toBe("login.reciprocate"); + }); + + it("login.start", async function() { + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.start', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {}); + expect(x.intent).toBe("login.start"); + }); + + it("parse and get", async function() { + const httpBackend = getHttpBackend(); + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.start', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "https://rz.server/123456" }, + }, + }), () => {}); + expect(x.intent).toBe("login.start"); + + const prom = x.channel.receive(); + httpBackend.when("GET", "https://rz.server/123456").response = { + body: {}, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual({}); + }); + }); +}); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts new file mode 100644 index 00000000000..a3a63c7f427 --- /dev/null +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -0,0 +1,287 @@ +/* +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 MockHttpBackend from "matrix-mock-request"; + +import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; + +describe("SimpleHttpRendezvousTransport", function() { + const getHttpBackend = (): MockHttpBackend => { + const httpBackend = new MockHttpBackend(); + SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); + return httpBackend; + }; + + async function postAndCheckLocation( + fallbackRzServer: string, + locationResponse: string, + expectedFinalLocation: string, + ) { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", fallbackRzServer).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: locationResponse, + }, + }, + }; + await httpBackend.flush(''); + await prom; + } + { // first GET without etag + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", expectedFinalLocation).response = { + body: {}, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({}); + httpBackend.verifyNoOutstandingRequests(); + httpBackend.verifyNoOutstandingExpectation(); + } + } + it("should throw an error when no server available", function() { + getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({}); + expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); + }); + + it("POST to fallback server", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + }); + + it("POST with absolute path response", async function() { + await postAndCheckLocation("https://fallbackserver/rz", "/123", "https://fallbackserver/123"); + }); + + it("POST with relative path response", async function() { + await postAndCheckLocation("https://fallbackserver/rz", "123", "https://fallbackserver/rz/123"); + }); + + it("POST with relative path response including parent", async function() { + await postAndCheckLocation("https://fallbackserver/rz/abc", "../xyz/123", "https://fallbackserver/rz/xyz/123"); + }); + + it("POST to follow 307 to other server", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 307, + headers: { + location: "https://redirected.fallbackserver/rz", + }, + }, + }; + httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://redirected.fallbackserver/rz/123", + etag: "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + }); + + it("POST and GET", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { + expect(headers["content-type"]).toEqual("application/json"); + expect(data).toEqual({ foo: "baa" }); + }).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + } + { // first GET without etag + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://fallbackserver/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + { // subsequent GET which should have etag from previous request + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers, data }) => { + expect(headers["if-none-match"]).toEqual("aaa"); + }).response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "bbb", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); + + it("POST and PUTs", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { + expect(headers["content-type"]).toEqual("application/json"); + expect(data).toEqual({ foo: "baa" }); + }).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + { // first PUT without etag + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ a: "b" })); + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + expect(headers["if-match"]).toBeUndefined(); + }).response = { + body: null, + response: { + statusCode: 202, + headers: { + "etag": "aaa", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + { // subsequent PUT which should have etag from previous request + const prom = simpleHttpTransport.receive(); + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + expect(headers["if-match"]).toEqual("aaa"); + }).response = { + body: null, + response: { + statusCode: 202, + headers: { + "etag": "bbb", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + }); + + it("init with URI", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + rendezvousUri: "https://server/rz/123", + }); + { + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://server/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); + + it("init from HS", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + rendezvousUri: "https://server/rz/123", + }); + { + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://server/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); +}); From 9e290142044fd2666f4b2074e44d6755ba3af9af Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:08:55 +0100 Subject: [PATCH 04/74] Make AES work on Node.js as well as browser --- src/rendezvous/channels/ecdhV1.ts | 52 ++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 99bcefded56..d875c51a985 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -28,6 +28,7 @@ import { } from '../index'; import { SecureRendezvousChannelAlgorithm } from '.'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; +import { getCrypto } from '../../utils'; const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? (window.crypto.subtle || window.crypto.webkitSubtle) : null; @@ -60,9 +61,13 @@ function generateDecimalSas(sasBytes: number[]): string { return digits.join('-'); } -async function importKey(key: Uint8Array): Promise { +async function importKey(key: Uint8Array): Promise { + if (getCrypto()) { + return key; + } + if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); + throw new Error('Neither Web Crypto nor Node.js crypto available'); } const imported = subtleCrypto.importKey( @@ -83,7 +88,7 @@ async function importKey(key: Uint8Array): Promise { export class ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; - private aesKey?: CryptoKey; + private aesKey?: CryptoKey | Uint8Array; public onFailure?: (reason: RendezvousFailureReason) => void; constructor( @@ -172,8 +177,21 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } private async encrypt(data: any): Promise { - if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); + if (this.aesKey instanceof Uint8Array) { + const crypto = getCrypto(); + + const iv = crypto.randomBytes(32); + const cipher = crypto.createCipheriv("aes-256-gcm", this.aesKey as Uint8Array, iv, { authTagLength: 16 }); + const ciphertext = Buffer.concat([ + cipher.update(data, "utf8"), + cipher.final(), + cipher.getAuthTag(), + ]); + + return JSON.stringify({ + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + }); } const iv = new Uint8Array(32); @@ -185,8 +203,9 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { { name: "AES-GCM", iv, + tagLength: 128, }, - this.aesKey, + this.aesKey as CryptoKey, encodedData, ); @@ -212,22 +231,31 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } private async decrypt({ iv, ciphertext }: { iv: string, ciphertext: string }): Promise { - if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); - } - if (!ciphertext || !iv) { throw new Error('Missing ciphertext and/or iv'); } const ciphertextBytes = decodeBase64(ciphertext); + if (this.aesKey instanceof Uint8Array) { + const crypto = getCrypto(); + // in contrast to Web Crypto API, Node's crypto needs the auth tag split off the cipher text + const ciphertextOnly = ciphertextBytes.slice(0, ciphertextBytes.length - 16); + const authTag = ciphertextBytes.slice(ciphertextBytes.length - 16); + const decipher = crypto.createDecipheriv( + "aes-256-gcm", this.aesKey as Uint8Array, decodeBase64(iv), { authTagLength: 16 }, + ); + decipher.setAuthTag(authTag); + return decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"); + } + const plaintext = await subtleCrypto.decrypt( { name: "AES-GCM", iv: decodeBase64(iv), + tagLength: 128, }, - this.aesKey, + this.aesKey as CryptoKey, ciphertextBytes, ); @@ -251,7 +279,7 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } const decrypted = await this.decrypt(data); logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); - return decrypted; + return JSON.parse(decrypted); } else if (this.aesKey) { throw new Error('Data received but no ciphertext'); } From e16487571b025efbe3d1a5b11ea20a32b7419c0d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:09:05 +0100 Subject: [PATCH 05/74] Tests for ECDH/X25519 --- spec/unit/rendezvous/ecdh.spec.ts | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spec/unit/rendezvous/ecdh.spec.ts diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts new file mode 100644 index 00000000000..61b434ad597 --- /dev/null +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -0,0 +1,123 @@ +/* +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 crypto from 'crypto'; + +import '../../olm-loader'; +import { + RendezvousFailureListener, + RendezvousFailureReason, + RendezvousIntent, + RendezvousTransport, + RendezvousTransportDetails, +} from "../../../src/rendezvous"; +import { ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; +import { decodeBase64 } from '../../../src/crypto/olmlib'; +import { setCrypto, sleep } from '../../../src/utils'; + +class DummyTransport implements RendezvousTransport { + otherParty?: DummyTransport; + etag?: string; + data = null; + + ready = false; + + onCancelled?: RendezvousFailureListener; + + details(): Promise { + return Promise.resolve({ + type: 'dummy', + }); + } + + async send(contentType: string, data: any): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag === this.etag) { + this.data = data; + this.etag = Math.random().toString(); + return; + } + await sleep(100); + } + } + + async receive(): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag !== this.etag) { + this.etag = this.otherParty?.etag; + return JSON.parse(this.otherParty.data); + } + await sleep(100); + } + } + + cancel(reason: RendezvousFailureReason): Promise { + throw new Error("Method not implemented."); + } +} + +describe("ECDHv1", function() { + beforeAll(async function() { + setCrypto(crypto); + await global.Olm.init(); + }); + + it("initiator wants to sign in", async function() { + const aliceTransport = new DummyTransport(); + const bobTransport = new DummyTransport(); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = "hello world"; + await alice.send(message); + const bobReceive = await bob.receive(); + expect(bobReceive).toEqual(message); + }); + + it("initiator wants to reciprocate", async function() { + const aliceTransport = new DummyTransport(); + const bobTransport = new DummyTransport(); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = "hello world"; + await bob.send(message); + const aliceReceive = await alice.receive(); + expect(aliceReceive).toEqual(message); + }); +}); From f6eb641371b2111caddeb03c0073baea22cc0de1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:18:32 +0100 Subject: [PATCH 06/74] stric mode linting --- spec/unit/rendezvous/ecdh.spec.ts | 2 +- src/rendezvous/channels/ecdhV1.ts | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 61b434ad597..223a23577c5 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -60,7 +60,7 @@ class DummyTransport implements RendezvousTransport { while (true) { if (!this.etag || this.otherParty?.etag !== this.etag) { this.etag = this.otherParty?.etag; - return JSON.parse(this.otherParty.data); + return this.otherParty?.data ? JSON.parse(this.otherParty.data) : undefined; } await sleep(100); } diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index d875c51a985..f116717d661 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -67,7 +67,7 @@ async function importKey(key: Uint8Array): Promise { } if (!subtleCrypto) { - throw new Error('Neither Web Crypto nor Node.js crypto available'); + throw new Error('Neither Web Crypto nor Node.js crypto are available'); } const imported = subtleCrypto.importKey( @@ -104,10 +104,6 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Code already generated'); } - const data = { - "algorithm": SecureRendezvousChannelAlgorithm.ECDH_V1, - }; - await this.send({ algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1 }); const rendezvous: ECDHv1RendezvousCode = { @@ -115,7 +111,6 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, key: encodeBase64(this.ourPublicKey), transport: await this.transport.details(), - ...data, }, intent, }; @@ -155,10 +150,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { }); } - this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey)); + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); - const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; - const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; + const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; let aesInfo = SecureRendezvousChannelAlgorithm.ECDH_V1.toString(); aesInfo += `|${encodeBase64(initiatorKey)}`; aesInfo += `|${encodeBase64(recipientKey)}`; @@ -168,7 +163,7 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { this.aesKey = await importKey(aesKeyBytes); logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); - logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey)}`); + logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey!)}`); logger.debug(`AES info: ${aesInfo}`); logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); @@ -194,6 +189,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { }); } + if (!subtleCrypto) { + throw new Error('Neither Web Crypto nor Node.js crypto are available'); + } + const iv = new Uint8Array(32); window.crypto.getRandomValues(iv); @@ -249,6 +248,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { return decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"); } + if (!subtleCrypto) { + throw new Error('Neither Web Crypto nor Node.js crypto are available'); + } + const plaintext = await subtleCrypto.decrypt( { name: "AES-GCM", From b9b923e2f7f68cea6a4350c5de7b8278730e4d74 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:21:57 +0100 Subject: [PATCH 07/74] Fix incorrect test --- spec/unit/rendezvous/simpleHttpTransport.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a3a63c7f427..a6d344cad54 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -209,8 +209,9 @@ describe("SimpleHttpRendezvousTransport", function() { } { // first PUT without etag const prom = simpleHttpTransport.send("application/json", JSON.stringify({ a: "b" })); - httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => { expect(headers["if-match"]).toBeUndefined(); + expect(data).toEqual({ a: "b" }); }).response = { body: null, response: { @@ -224,7 +225,7 @@ describe("SimpleHttpRendezvousTransport", function() { await prom; } { // subsequent PUT which should have etag from previous request - const prom = simpleHttpTransport.receive(); + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ c: "d" })); httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { expect(headers["if-match"]).toEqual("aaa"); }).response = { From 5e27b8cca3ac291eae3e9dd6076a6f9fcd82c8cc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 21:42:05 +0100 Subject: [PATCH 08/74] Refactor full rendezvous logic out of react-sdk into js-sdk --- src/rendezvous/index.ts | 1 + src/rendezvous/rendezvous.ts | 362 +++++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/rendezvous/rendezvous.ts diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 2a741243ac5..cd0fd673c12 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -27,6 +27,7 @@ export * from './code'; export * from './cancellationReason'; export * from './transport'; export * from './channel'; +export * from './rendezvous'; /** * Attempts to parse the given code as a rendezvous and return a channel and transport. diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts new file mode 100644 index 00000000000..65b16192835 --- /dev/null +++ b/src/rendezvous/rendezvous.ts @@ -0,0 +1,362 @@ +/* +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 { RendezvousChannel } from "."; +import { LoginTokenPostResponse } from "../@types/auth"; +import { MatrixClient } from "../client"; +import { CrossSigningInfo, requestKeysDuringVerification } from "../crypto/CrossSigning"; +import { DeviceInfo } from "../crypto/deviceinfo"; +import { IAuthData } from "../interactive-auth"; +import { logger } from "../logger"; +import { createClient } from "../matrix"; +import { sleep } from "../utils"; +import { RendezvousFailureReason } from "./cancellationReason"; +import { RendezvousIntent } from "./code"; + +export enum PayloadType { + Start = 'm.login.start', + Finish = 'm.login.finish', + Progress = 'm.login.progress', +} + +export class Rendezvous { + private cli?: MatrixClient; + private newDeviceId?: string; + private newDeviceKey?: string; + private ourIntent: RendezvousIntent; + public code?: string; + public onFailure?: (reason: RendezvousFailureReason) => void; + + constructor(public channel: RendezvousChannel, cli?: MatrixClient) { + this.cli = cli; + this.ourIntent = this.isNewDevice ? + RendezvousIntent.LOGIN_ON_NEW_DEVICE : + RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; + } + + async generateCode(): Promise { + if (this.code) { + return; + } + + this.code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + + private get isNewDevice(): boolean { + return !this.cli; + } + + private async areIntentsIncompatible(theirIntent: RendezvousIntent): Promise { + const incompatible = theirIntent === this.ourIntent; + + logger.info(`ourIntent: ${this.ourIntent}, theirIntent: ${theirIntent}, incompatible: ${incompatible}`); + + if (incompatible) { + await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); + await this.channel.cancel( + this.isNewDevice ? + RendezvousFailureReason.OtherDeviceNotSignedIn : + RendezvousFailureReason.OtherDeviceAlreadySignedIn, + ); + } + + return incompatible; + } + + async startAfterShowingCode(): Promise { + return this.start(); + } + + async startAfterScanningCode(theirIntent: RendezvousIntent): Promise { + return this.start(theirIntent); + } + + private async start(theirIntent?: RendezvousIntent): Promise { + const didScan = !!theirIntent; + + const checksum = await this.channel.connect(); + + logger.info(`Connected to secure channel with checksum: ${checksum}`); + + if (didScan) { + if (await this.areIntentsIncompatible(theirIntent)) { + // a m.login.finish event is sent as part of areIntentsIncompatible + return undefined; + } + } + + if (this.cli) { + if (didScan) { + await this.channel.receive(); // wait for ack + } + + // determine available protocols + if (!(await this.cli.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { + logger.info("Server doesn't support MSC3882"); + await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); + return undefined; + } + + await this.send({ type: PayloadType.Progress, protocols: ['login_token'] }); + + logger.info('Waiting for other device to chose protocol'); + const { type, protocol, outcome } = await this.channel.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') { + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + } else { + if (!didScan) { + logger.info("Sending ack"); + await this.send({ type: PayloadType.Progress }); + } + + logger.info("Waiting for protocols"); + const { protocols } = await this.channel.receive(); + + if (!Array.isArray(protocols) || !protocols.includes('login_token')) { + await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + + await this.send({ type: PayloadType.Progress, protocol: "login_token" }); + } + + return checksum; + } + + async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { + await this.channel.send({ type, ...payload }); + } + + async completeLoginOnNewDevice(): Promise<{ + userId: string; + deviceId: string; + accessToken: string; + homeserverUrl: string; + } | undefined> { + logger.info('Waiting for login_token'); + + // eslint-disable-next-line camelcase + const { type, login_token: token, outcome, homeserver } = await this.channel.receive(); + + if (type === PayloadType.Finish) { + switch (outcome ?? '') { + case 'unsupported': + await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); + break; + default: + await this.cancel(RendezvousFailureReason.Unknown); + } + return undefined; + } + + if (!homeserver) { + throw new Error("No homeserver returned"); + } + // eslint-disable-next-line camelcase + if (!token) { + throw new Error("No login token returned"); + } + + const client = createClient({ + baseUrl: homeserver, + }); + + const { device_id: deviceId, user_id: userId, access_token: accessToken } = + await client.login("m.login.token", { token }); + + return { + userId, + deviceId, + accessToken, + homeserverUrl: homeserver, + }; + } + + async completeVerificationOnNewDevice(client: MatrixClient): Promise { + await this.send({ + type: PayloadType.Progress, + outcome: 'success', + device_id: client.getDeviceId(), + device_key: client.getDeviceEd25519Key(), + }); + + // await confirmation of verification + const { + verifying_device_id: verifyingDeviceId, + master_key: masterKey, + verifying_device_key: verifyingDeviceKey, + } = await this.channel.receive(); + + const userId = client.getUserId()!; + const verifyingDeviceFromServer = + client.crypto.deviceList.getStoredDevice(userId, verifyingDeviceId); + + if (verifyingDeviceFromServer?.getFingerprint() === verifyingDeviceKey) { + // set other device as verified + logger.info(`Setting device ${verifyingDeviceId} as verified`); + await client.setDeviceVerified(userId, verifyingDeviceId, true); + + if (masterKey) { + // set master key as trusted + await client.setDeviceVerified(userId, masterKey, true); + } + + // request secrets from the verifying device + logger.info(`Requesting secrets from ${verifyingDeviceId}`); + await requestKeysDuringVerification(client, userId, verifyingDeviceId); + } else { + logger.info(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + } + } + + async declineLoginOnExistingDevice() { + logger.info('User declined linking'); + await this.send({ type: PayloadType.Finish, outcome: 'declined' }); + } + + async confirmLoginOnExistingDevice(): Promise { + const client = this.cli; + + logger.info("Requesting login token"); + + const loginTokenResponse = await client.requestLoginToken(); + + if (typeof (loginTokenResponse as IAuthData).session === 'string') { + // TODO: handle UIA response + throw new Error("UIA isn't supported yet"); + } + // eslint-disable-next-line camelcase + const { login_token } = loginTokenResponse as LoginTokenPostResponse; + + // eslint-disable-next-line camelcase + await this.send({ type: PayloadType.Progress, login_token, homeserver: client.baseUrl }); + + logger.info('Waiting for outcome'); + const res = await this.channel.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 checkAndCrossSignDevice(deviceInfo: DeviceInfo) { + // 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()}`, + ); + } + + // mark the device as verified locally + cross sign + logger.info(`Marking device ${this.newDeviceId} as verified`); + const info = await this.cli.crypto.setDeviceVerification( + this.cli.getUserId(), + this.newDeviceId, + true, false, true, + ); + + const masterPublicKey = this.cli.crypto.crossSigningInfo.getId('master'); + + await this.send({ + type: PayloadType.Finish, + outcome: 'verified', + verifying_device_id: this.cli.getDeviceId(), + verifying_device_key: this.cli.getDeviceEd25519Key(), + master_key: masterPublicKey, + }); + + return info; + } + + async crossSign(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; + } + + const cli = this.cli; + + { + const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + + if (deviceInfo) { + return await this.checkAndCrossSignDevice(deviceInfo); + } + } + + logger.info("New device is not online"); + await sleep(timeout); + + logger.info("Going to wait for new device to be online"); + + { + const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + + if (deviceInfo) { + return await this.checkAndCrossSignDevice(deviceInfo); + } + } + + throw new Error('Device not online within timeout'); + } + + async userCancelled(): Promise { + this.cancel(RendezvousFailureReason.UserCancelled); + } + + async cancel(reason: RendezvousFailureReason) { + await this.channel.cancel(reason); + } + + async close() { + await this.channel.close(); + } +} From be3c2a718a5495262af57b0169926bb716c2bc2e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 22:48:31 +0100 Subject: [PATCH 09/74] Use correct unstable import --- src/rendezvous/transports/simpleHttpTransport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 14cd9e5e44e..a8a019d7701 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -19,7 +19,7 @@ import { sleep } from '../../utils'; import { RendezvousFailureListener, RendezvousFailureReason } from '../cancellationReason'; import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; -import { PREFIX_UNSTABLE } from '../../http-api'; +import { ClientPrefix } from '../../http-api'; export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { type: 'http.v1'; @@ -96,7 +96,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.client) { try { if (await this.client.doesServerSupportUnstableFeature('org.matrix.msc3886')) { - return `${this.client.baseUrl}${PREFIX_UNSTABLE}/org.matrix.msc3886/rendezvous`; + return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; } } catch (err) { logger.warn('Failed to get unstable features', err); From a98c7aa6f297d7df3dc17f0af17853c722eb5630 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:12:42 +0100 Subject: [PATCH 10/74] Pass fetch around --- spec/unit/rendezvous/rendezvous.spec.ts | 24 +++++++++---------- .../rendezvous/simpleHttpTransport.spec.ts | 13 +++++++--- src/rendezvous/index.ts | 3 ++- .../transports/simpleHttpTransport.ts | 23 ++++++------------ 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index d948731d482..a87339ba063 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; import { buildChannelFromCode } from "../../../src/rendezvous"; -import { SimpleHttpRendezvousTransport } from '../../../src/rendezvous/transports'; describe("Rendezvous", function() { beforeAll(async function() { @@ -27,41 +26,42 @@ describe("Rendezvous", function() { const getHttpBackend = (): MockHttpBackend => { const httpBackend = new MockHttpBackend(); - SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); return httpBackend; }; + const fetch = getHttpBackend().fetchFn as typeof global.fetch; + describe("buildChannelFromCode", function() { it("non-JSON", function() { - expect(buildChannelFromCode("xyz", () => {})).rejects.toThrow("Invalid code"); + expect(buildChannelFromCode("xyz", () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("invalid JSON", function() { - expect(buildChannelFromCode(JSON.stringify({}), () => {})).rejects.toThrow("Unsupported transport"); + expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetch)).rejects.toThrow("Unsupported transport"); }); it("invalid transport type", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "foo" } }, - }), () => {})).rejects.toThrow("Unsupported transport"); + }), () => {}, fetch)).rejects.toThrow("Unsupported transport"); }); it("missing URI", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1" } }, - }), () => {})).rejects.toThrow("Invalid code"); + }), () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("invalid URI field", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: false } }, - }), () => {})).rejects.toThrow("Invalid code"); + }), () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("missing intent", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: "something" } }, - }), () => {})).rejects.toThrow("Invalid intent"); + }), () => {}, fetch)).rejects.toThrow("Invalid intent"); }); it("invalid intent", function() { @@ -72,7 +72,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {})).rejects.toThrow("Invalid intent"); + }), () => {}, fetch)).rejects.toThrow("Invalid intent"); }); it("login.reciprocate", async function() { @@ -83,7 +83,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.reciprocate"); }); @@ -95,7 +95,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.start"); }); @@ -108,7 +108,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "https://rz.server/123456" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.start"); const prom = x.channel.receive(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a6d344cad54..9c97e14d95c 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -21,17 +21,18 @@ import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transport describe("SimpleHttpRendezvousTransport", function() { const getHttpBackend = (): MockHttpBackend => { const httpBackend = new MockHttpBackend(); - SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); return httpBackend; }; + const fetch = getHttpBackend().fetchFn as typeof global.fetch; + async function postAndCheckLocation( fallbackRzServer: string, locationResponse: string, expectedFinalLocation: string, ) { const httpBackend = getHttpBackend(); - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,7 +66,7 @@ describe("SimpleHttpRendezvousTransport", function() { } it("should throw an error when no server available", function() { getHttpBackend(); - const simpleHttpTransport = new SimpleHttpRendezvousTransport({}); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); @@ -73,6 +74,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -104,6 +106,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -133,6 +136,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -189,6 +193,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -246,6 +251,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", + fetch, }); { const prom = simpleHttpTransport.receive(); @@ -268,6 +274,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", + fetch, }); { const prom = simpleHttpTransport.receive(); diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index cd0fd673c12..a12392a2284 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -38,6 +38,7 @@ export * from './rendezvous'; export async function buildChannelFromCode( code: string, onFailure: RendezvousFailureListener, + fetch: typeof global.fetch, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -62,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri }); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetch }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index a8a019d7701..99b7c7d8cb9 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,19 +40,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - - private static fetch(resource: URL | string, options?: RequestInit): ReturnType { - if (this.fetchFn) { - return this.fetchFn(resource, options); - } - return global.fetch(resource, options); - } - - private static fetchFn?: typeof global.fetch; - - public static setFetchFn(fetchFn: typeof global.fetch): void { - SimpleHttpRendezvousTransport.fetchFn = fetchFn; - } + private fetch: typeof global.fetch; constructor({ onFailure, @@ -60,13 +48,16 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { hsUrl, fallbackRzServer, rendezvousUri, + fetch, }: { + fetch: typeof global.fetch; onFailure?: RendezvousFailureListener; client?: MatrixClient; hsUrl?: string; fallbackRzServer?: string; rendezvousUri?: string; }) { + this.fetch = fetch; this.onFailure = onFailure; this.client = client; this.hsUrl = hsUrl; @@ -124,7 +115,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { headers['if-match'] = this.etag; } - const res = await SimpleHttpRendezvousTransport.fetch(uri, { method, + const res = await this.fetch(uri, { method, headers, body: data, }); @@ -167,7 +158,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.etag) { headers['if-none-match'] = this.etag; } - const poll = await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "GET", headers }); + const poll = await this.fetch(this.uri, { method: "GET", headers }); logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); if (poll.status === 404) { @@ -201,7 +192,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.uri && reason === RendezvousFailureReason.UserDeclined) { try { logger.debug(`Deleting channel: ${this.uri}`); - await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "DELETE" }); + await this.fetch(this.uri, { method: "DELETE" }); } catch (e) { logger.warn(e); } From ceaa489cf7b120b1976da603b4915d0813fc060d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 12:41:04 +0100 Subject: [PATCH 11/74] Make correct usage of fetch in tests --- spec/unit/rendezvous/rendezvous.spec.ts | 12 +++++------ .../rendezvous/simpleHttpTransport.spec.ts | 21 +++++++------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index a87339ba063..ea994d9c41b 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -24,12 +24,13 @@ describe("Rendezvous", function() { await global.Olm.init(); }); - const getHttpBackend = (): MockHttpBackend => { - const httpBackend = new MockHttpBackend(); - return httpBackend; - }; + let httpBackend: MockHttpBackend; + let fetch: typeof global.fetch; - const fetch = getHttpBackend().fetchFn as typeof global.fetch; + beforeEach(function() { + httpBackend = new MockHttpBackend(); + fetch = httpBackend.fetchFn as typeof global.fetch; + }); describe("buildChannelFromCode", function() { it("non-JSON", function() { @@ -100,7 +101,6 @@ describe("Rendezvous", function() { }); it("parse and get", async function() { - const httpBackend = getHttpBackend(); const x = await buildChannelFromCode(JSON.stringify({ intent: 'login.start', rendezvous: { diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 9c97e14d95c..a093b1e0c52 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -19,19 +19,19 @@ import MockHttpBackend from "matrix-mock-request"; import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; describe("SimpleHttpRendezvousTransport", function() { - const getHttpBackend = (): MockHttpBackend => { - const httpBackend = new MockHttpBackend(); - return httpBackend; - }; + let httpBackend: MockHttpBackend; + let fetch: typeof global.fetch; - const fetch = getHttpBackend().fetchFn as typeof global.fetch; + beforeEach(function() { + httpBackend = new MockHttpBackend(); + fetch = httpBackend.fetchFn as typeof global.fetch; + }); async function postAndCheckLocation( fallbackRzServer: string, locationResponse: string, expectedFinalLocation: string, ) { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); @@ -65,13 +65,11 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -103,7 +101,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST to follow 307 to other server", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -133,7 +130,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and GET", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -172,7 +168,7 @@ describe("SimpleHttpRendezvousTransport", function() { } { // subsequent GET which should have etag from previous request const prom = simpleHttpTransport.receive(); - httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers, data }) => { + httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => { expect(headers["if-none-match"]).toEqual("aaa"); }).response = { body: { foo: "baa" }, @@ -190,7 +186,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and PUTs", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -248,7 +243,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init with URI", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetch, @@ -271,7 +265,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init from HS", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetch, From 6eb04e0a67f40db3b23ef4ecf6d90465ad3b7f4b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:05:11 +0100 Subject: [PATCH 12/74] fix: you can't call fetch when it's not on window --- spec/unit/rendezvous/rendezvous.spec.ts | 25 ++++++++++--------- .../rendezvous/simpleHttpTransport.spec.ts | 20 +++++++-------- src/rendezvous/index.ts | 4 +-- .../transports/simpleHttpTransport.ts | 15 ++++++++--- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index ea994d9c41b..6c1ecbd7c28 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -25,44 +25,45 @@ describe("Rendezvous", function() { }); let httpBackend: MockHttpBackend; - let fetch: typeof global.fetch; + let fetchFn: typeof global.fetchFn; beforeEach(function() { httpBackend = new MockHttpBackend(); - fetch = httpBackend.fetchFn as typeof global.fetch; + fetchFn = httpBackend.fetchFn as typeof global.fetch; }); describe("buildChannelFromCode", function() { it("non-JSON", function() { - expect(buildChannelFromCode("xyz", () => {}, fetch)).rejects.toThrow("Invalid code"); + expect(buildChannelFromCode("xyz", () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("invalid JSON", function() { - expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetch)).rejects.toThrow("Unsupported transport"); + expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetchFn)) + .rejects.toThrow("Unsupported transport"); }); it("invalid transport type", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "foo" } }, - }), () => {}, fetch)).rejects.toThrow("Unsupported transport"); + }), () => {}, fetchFn)).rejects.toThrow("Unsupported transport"); }); it("missing URI", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1" } }, - }), () => {}, fetch)).rejects.toThrow("Invalid code"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("invalid URI field", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: false } }, - }), () => {}, fetch)).rejects.toThrow("Invalid code"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("missing intent", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: "something" } }, - }), () => {}, fetch)).rejects.toThrow("Invalid intent"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); }); it("invalid intent", function() { @@ -73,7 +74,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch)).rejects.toThrow("Invalid intent"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); }); it("login.reciprocate", async function() { @@ -84,7 +85,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.reciprocate"); }); @@ -96,7 +97,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.start"); }); @@ -108,7 +109,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "https://rz.server/123456" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.start"); const prom = x.channel.receive(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a093b1e0c52..2c5f6ee7dab 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -20,11 +20,11 @@ import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transport describe("SimpleHttpRendezvousTransport", function() { let httpBackend: MockHttpBackend; - let fetch: typeof global.fetch; + let fetchFn: typeof global.fetch; beforeEach(function() { httpBackend = new MockHttpBackend(); - fetch = httpBackend.fetchFn as typeof global.fetch; + fetchFn = httpBackend.fetchFn as typeof global.fetch; }); async function postAndCheckLocation( @@ -32,7 +32,7 @@ describe("SimpleHttpRendezvousTransport", function() { locationResponse: string, expectedFinalLocation: string, ) { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,14 +65,14 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetchFn }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -103,7 +103,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST to follow 307 to other server", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -132,7 +132,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST and GET", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -188,7 +188,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST and PUTs", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -245,7 +245,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("init with URI", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", - fetch, + fetchFn, }); { const prom = simpleHttpTransport.receive(); @@ -267,7 +267,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("init from HS", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", - fetch, + fetchFn, }); { const prom = simpleHttpTransport.receive(); diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index a12392a2284..646b9dc3975 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -38,7 +38,7 @@ export * from './rendezvous'; export async function buildChannelFromCode( code: string, onFailure: RendezvousFailureListener, - fetch: typeof global.fetch, + fetchFn?: typeof global.fetch, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -63,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetch }); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 99b7c7d8cb9..5816320c46a 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,7 +40,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - private fetch: typeof global.fetch; + private fetchFn: typeof global.fetch; constructor({ onFailure, @@ -48,16 +48,16 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { hsUrl, fallbackRzServer, rendezvousUri, - fetch, + fetchFn, }: { - fetch: typeof global.fetch; + fetchFn?: typeof global.fetch; onFailure?: RendezvousFailureListener; client?: MatrixClient; hsUrl?: string; fallbackRzServer?: string; rendezvousUri?: string; }) { - this.fetch = fetch; + this.fetchFn = fetchFn; this.onFailure = onFailure; this.client = client; this.hsUrl = hsUrl; @@ -77,6 +77,13 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { }; } + 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 { if (!this.client && this.hsUrl) { this.client = new MatrixClient({ From c8f260a734400043a40c0839f7442a8b1d411a2c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:16:19 +0100 Subject: [PATCH 13/74] Use class names to make it clearer that these are unstable MSC implementations --- spec/unit/rendezvous/ecdh.spec.ts | 10 +++++----- .../rendezvous/simpleHttpTransport.spec.ts | 18 +++++++++--------- src/rendezvous/channels/ecdhV1.ts | 2 +- src/rendezvous/index.ts | 10 +++++----- src/rendezvous/rendezvous.ts | 4 ++-- .../transports/simpleHttpTransport.ts | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 223a23577c5..7cfbc3c96f0 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -24,7 +24,7 @@ import { RendezvousTransport, RendezvousTransportDetails, } from "../../../src/rendezvous"; -import { ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; +import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; import { setCrypto, sleep } from '../../../src/utils'; @@ -84,9 +84,9 @@ describe("ECDHv1", function() { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -106,9 +106,9 @@ describe("ECDHv1", function() { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 2c5f6ee7dab..26ed94ef2e9 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; -import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; +import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; describe("SimpleHttpRendezvousTransport", function() { let httpBackend: MockHttpBackend; @@ -32,7 +32,7 @@ describe("SimpleHttpRendezvousTransport", function() { locationResponse: string, expectedFinalLocation: string, ) { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,12 +65,12 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetchFn }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fetchFn }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -101,7 +101,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST to follow 307 to other server", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -130,7 +130,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and GET", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -186,7 +186,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and PUTs", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -243,7 +243,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init with URI", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetchFn, }); @@ -265,7 +265,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init from HS", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetchFn, }); diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index f116717d661..1b852ed9f86 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -85,7 +85,7 @@ async function importKey(key: Uint8Array): Promise { * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) * X25519/ECDH key agreement based secure rendezvous channel. */ -export class ECDHv1RendezvousChannel implements RendezvousChannel { +export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey | Uint8Array; diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 646b9dc3975..437909eb3d5 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -18,9 +18,9 @@ import { RendezvousFailureListener, RendezvousFailureReason } from './cancellati import { RendezvousChannel } from './channel'; import { RendezvousCode, RendezvousIntent } from './code'; import { RendezvousError } from './error'; -import { SimpleHttpRendezvousTransport, SimpleHttpRendezvousTransportDetails } from './transports'; +import { MSC3886SimpleHttpRendezvousTransport, MSC3886SimpleHttpRendezvousTransportDetails } from './transports'; import { decodeBase64 } from '../crypto/olmlib'; -import { ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; +import { MSC3903ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; import { logger } from '../logger'; export * from './code'; @@ -53,7 +53,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedTransport); } - const transportDetails = rendezvous.transport as SimpleHttpRendezvousTransportDetails; + const transportDetails = rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails; if (typeof transportDetails.uri !== 'string') { throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); @@ -63,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); + const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); @@ -75,7 +75,7 @@ export async function buildChannelFromCode( logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); return { - channel: new ECDHv1RendezvousChannel(transport, theirPublicKey), + channel: new MSC3903ECDHv1RendezvousChannel(transport, theirPublicKey), intent, }; } diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 65b16192835..1bbf51bccc2 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -26,13 +26,13 @@ import { sleep } from "../utils"; import { RendezvousFailureReason } from "./cancellationReason"; import { RendezvousIntent } from "./code"; -export enum PayloadType { +enum PayloadType { Start = 'm.login.start', Finish = 'm.login.finish', Progress = 'm.login.progress', } -export class Rendezvous { +export class MSC3906Rendezvous { private cli?: MatrixClient; private newDeviceId?: string; private newDeviceKey?: string; diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 5816320c46a..87a772fd7cb 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -21,7 +21,7 @@ import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; import { ClientPrefix } from '../../http-api'; -export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { +export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { type: 'http.v1'; uri: string; } @@ -30,7 +30,7 @@ export interface SimpleHttpRendezvousTransportDetails extends RendezvousTranspor * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) * simple HTTP rendezvous protocol. */ -export class SimpleHttpRendezvousTransport implements RendezvousTransport { +export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { ready = false; cancelled = false; private uri?: string; @@ -66,7 +66,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { this.ready = !!this.uri; } - async details(): Promise { + async details(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } From 93ad97c4a4a198e8e8159bfaa904fa3190581d1d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:24:55 +0100 Subject: [PATCH 14/74] Linting --- src/rendezvous/index.ts | 6 +- src/rendezvous/rendezvous.ts | 77 ++++++++++++++----- .../transports/simpleHttpTransport.ts | 2 +- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 437909eb3d5..a7b5c6fee31 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -63,7 +63,11 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); + const transport = new MSC3886SimpleHttpRendezvousTransport({ + onFailure, + rendezvousUri: transportDetails.uri, + fetchFn, + }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 1bbf51bccc2..3621729853b 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -222,26 +222,31 @@ export class MSC3906Rendezvous { verifying_device_key: verifyingDeviceKey, } = await this.channel.receive(); + if (!verifyingDeviceId || !verifyingDeviceKey) { + logger.warn("No verifying_device_id or verifying_device_key received"); + return; + } + const userId = client.getUserId()!; const verifyingDeviceFromServer = - client.crypto.deviceList.getStoredDevice(userId, verifyingDeviceId); - - if (verifyingDeviceFromServer?.getFingerprint() === verifyingDeviceKey) { - // set other device as verified - logger.info(`Setting device ${verifyingDeviceId} as verified`); - await client.setDeviceVerified(userId, verifyingDeviceId, true); + client.crypto?.deviceList.getStoredDevice(userId, verifyingDeviceId); - if (masterKey) { - // set master key as trusted - await client.setDeviceVerified(userId, masterKey, true); - } + if (verifyingDeviceFromServer?.getFingerprint() !== verifyingDeviceKey) { + logger.warn(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + return; + } + // set other device as verified + logger.info(`Setting device ${verifyingDeviceId} as verified`); + await client.setDeviceVerified(userId, verifyingDeviceId, true); - // request secrets from the verifying device - logger.info(`Requesting secrets from ${verifyingDeviceId}`); - await requestKeysDuringVerification(client, userId, verifyingDeviceId); - } else { - logger.info(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + if (masterKey) { + // set master key as trusted + await client.setDeviceVerified(userId, masterKey, true); } + + // request secrets from the verifying device + logger.info(`Requesting secrets from ${verifyingDeviceId}`); + await requestKeysDuringVerification(client, userId, verifyingDeviceId); } async declineLoginOnExistingDevice() { @@ -250,6 +255,10 @@ export class MSC3906Rendezvous { } async confirmLoginOnExistingDevice(): Promise { + if (!this.cli) { + throw new Error('No client set'); + } + const client = this.cli; logger.info("Requesting login token"); @@ -284,6 +293,18 @@ export class MSC3906Rendezvous { } private async checkAndCrossSignDevice(deviceInfo: DeviceInfo) { + if (!this.cli) { + throw new Error('No client set'); + } + + if (!this.cli.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( @@ -291,10 +312,15 @@ export class MSC3906Rendezvous { ); } + const userId = this.cli.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.cli.crypto.setDeviceVerification( - this.cli.getUserId(), + userId, this.newDeviceId, true, false, true, ); @@ -321,11 +347,24 @@ export class MSC3906Rendezvous { logger.info("No new device key to sign"); return undefined; } + const client = this.cli; + + if (!client) { + throw new Error('No client set'); + } - const cli = this.cli; + if (!client.crypto) { + throw new Error('Crypto not available on client'); + } + + const userId = client.getUserId(); + + if (!userId) { + throw new Error('No user ID set'); + } { - const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.checkAndCrossSignDevice(deviceInfo); @@ -338,7 +377,7 @@ export class MSC3906Rendezvous { logger.info("Going to wait for new device to be online"); { - const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.checkAndCrossSignDevice(deviceInfo); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 87a772fd7cb..12433fa812a 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,7 +40,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - private fetchFn: typeof global.fetch; + private fetchFn?: typeof global.fetch; constructor({ onFailure, From 06566e1d30250978e3473eff110a8a56a02a335b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 10:17:59 +0100 Subject: [PATCH 15/74] Clean implementation of MSC3886 and MSC3903 --- src/rendezvous/cancellationReason.ts | 31 ++ src/rendezvous/channel.ts | 54 ++++ src/rendezvous/channels/ecdhV1.ts | 276 ++++++++++++++++++ src/rendezvous/channels/index.ts | 21 ++ src/rendezvous/code.ts | 31 ++ src/rendezvous/error.ts | 23 ++ src/rendezvous/index.ts | 84 ++++++ src/rendezvous/transport.ts | 59 ++++ src/rendezvous/transports/index.ts | 17 ++ .../transports/simpleHttpTransport.ts | 193 ++++++++++++ 10 files changed, 789 insertions(+) create mode 100644 src/rendezvous/cancellationReason.ts create mode 100644 src/rendezvous/channel.ts create mode 100644 src/rendezvous/channels/ecdhV1.ts create mode 100644 src/rendezvous/channels/index.ts create mode 100644 src/rendezvous/code.ts create mode 100644 src/rendezvous/error.ts create mode 100644 src/rendezvous/index.ts create mode 100644 src/rendezvous/transport.ts create mode 100644 src/rendezvous/transports/index.ts create mode 100644 src/rendezvous/transports/simpleHttpTransport.ts diff --git a/src/rendezvous/cancellationReason.ts b/src/rendezvous/cancellationReason.ts new file mode 100644 index 00000000000..c23168dd154 --- /dev/null +++ b/src/rendezvous/cancellationReason.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/src/rendezvous/channel.ts b/src/rendezvous/channel.ts new file mode 100644 index 00000000000..256016af921 --- /dev/null +++ b/src/rendezvous/channel.ts @@ -0,0 +1,54 @@ +/* +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, + RendezvousTransport, + RendezvousIntent, + RendezvousFailureReason, +} from "."; + +export interface RendezvousChannel { + transport: RendezvousTransport; + /** + * @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: any): Promise; + + /** + * Receive a payload from the channel. + * @returns the received payload + */ + receive(): Promise; + + /** + * 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/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts new file mode 100644 index 00000000000..99bcefded56 --- /dev/null +++ b/src/rendezvous/channels/ecdhV1.ts @@ -0,0 +1,276 @@ +/* +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 { SAS } from '@matrix-org/olm'; + +import { logger } from '../../logger'; +import { RendezvousError } from '../error'; +import { + RendezvousCode, + RendezvousIntent, + RendezvousChannel, + RendezvousTransportDetails, + RendezvousTransport, + RendezvousFailureReason, +} from '../index'; +import { SecureRendezvousChannelAlgorithm } from '.'; +import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; + +const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? + (window.crypto.subtle || window.crypto.webkitSubtle) : null; + +export interface ECDHv1RendezvousCode extends RendezvousCode { + rendezvous: { + transport: RendezvousTransportDetails; + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1; + key: string; + }; +} + +// The underlying algorithm is the same as: +// https://github.com/matrix-org/matrix-js-sdk/blob/75204d5cd04d67be100fca399f83b1a66ffb8118/src/crypto/verification/SAS.ts#L54-L68 +function generateDecimalSas(sasBytes: number[]): string { + /** + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + const digits = [ + (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, + ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, + ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000, + ]; + + return digits.join('-'); +} + +async function importKey(key: Uint8Array): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto 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. + */ +export class ECDHv1RendezvousChannel implements RendezvousChannel { + private olmSAS?: SAS; + private ourPublicKey: Uint8Array; + private aesKey?: CryptoKey; + public onFailure?: (reason: RendezvousFailureReason) => void; + + constructor( + public transport: RendezvousTransport, + private theirPublicKey?: Uint8Array, + ) { + 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'); + } + + const data = { + "algorithm": SecureRendezvousChannelAlgorithm.ECDH_V1, + }; + + await this.send({ algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1 }); + + const rendezvous: ECDHv1RendezvousCode = { + "rendezvous": { + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + key: encodeBase64(this.ourPublicKey), + transport: await this.transport.details(), + ...data, + }, + intent, + }; + + return rendezvous; + } + + async connect(): Promise { + 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 + logger.info('Waiting for other device to send their public key'); + const res = await this.receive(); + if (!res) { + throw new Error('No response from other device'); + } + const { key, algorithm } = res; + + if (algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1 || (isInitiator && !key)) { + throw new RendezvousError( + 'Unsupported algorithm: ' + algorithm, + RendezvousFailureReason.UnsupportedAlgorithm, + ); + } + + this.theirPublicKey = decodeBase64(key); + } else { + // send our public key unencrypted + await this.send({ + algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + key: encodeBase64(this.ourPublicKey), + }); + } + + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey)); + + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; + const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + let aesInfo = SecureRendezvousChannelAlgorithm.ECDH_V1.toString(); + aesInfo += `|${encodeBase64(initiatorKey)}`; + aesInfo += `|${encodeBase64(recipientKey)}`; + + const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); + + this.aesKey = await importKey(aesKeyBytes); + + logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); + logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey)}`); + logger.debug(`AES info: ${aesInfo}`); + logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); + + const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); + return generateDecimalSas(Array.from(rawChecksum)); + } + + private async encrypt(data: any): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto not available'); + } + + const iv = new Uint8Array(32); + window.crypto.getRandomValues(iv); + + const encodedData = new TextEncoder().encode(data); + + const ciphertext = await subtleCrypto.encrypt( + { + name: "AES-GCM", + iv, + }, + this.aesKey, + encodedData, + ); + + return JSON.stringify({ + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + }); + } + + public async send(data: any) { + if (!this.olmSAS) { + throw new Error('Channel closed'); + } + + const stringifiedData = JSON.stringify(data); + + if (this.aesKey) { + logger.info(`Encrypting: ${stringifiedData}`); + await this.transport.send('application/json', await this.encrypt(stringifiedData)); + } else { + await this.transport.send('application/json', stringifiedData); + } + } + + private async decrypt({ iv, ciphertext }: { iv: string, ciphertext: string }): Promise { + if (!subtleCrypto) { + throw new Error('Subtle crypto not available'); + } + + if (!ciphertext || !iv) { + throw new Error('Missing ciphertext and/or iv'); + } + + const ciphertextBytes = decodeBase64(ciphertext); + + const plaintext = await subtleCrypto.decrypt( + { + name: "AES-GCM", + iv: decodeBase64(iv), + }, + this.aesKey, + ciphertextBytes, + ); + + return new TextDecoder().decode(new Uint8Array(plaintext)); + } + + public async receive(): Promise { + if (!this.olmSAS) { + throw new Error('Channel closed'); + } + + const data = await this.transport.receive(); + logger.info(`Received data: ${JSON.stringify(data)}`); + if (!data) { + return data; + } + + if (data.ciphertext) { + if (!this.aesKey) { + throw new Error('Shared secret not set up'); + } + const decrypted = await this.decrypt(data); + logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); + return decrypted; + } else if (this.aesKey) { + throw new Error('Data received but no ciphertext'); + } + + return data; + } + + public async close() { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + + public async cancel(reason: RendezvousFailureReason) { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts new file mode 100644 index 00000000000..059223029dc --- /dev/null +++ b/src/rendezvous/channels/index.ts @@ -0,0 +1,21 @@ +/* +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 './ecdhV1'; + +export enum SecureRendezvousChannelAlgorithm { + ECDH_V1 = "m.rendezvous.v1.curve25519-aes-sha256" +} diff --git a/src/rendezvous/code.ts b/src/rendezvous/code.ts new file mode 100644 index 00000000000..c77379ba6ac --- /dev/null +++ b/src/rendezvous/code.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. +*/ + +import { SecureRendezvousChannelAlgorithm } from "./channels"; +import { RendezvousTransportDetails } from "./transport"; + +export enum RendezvousIntent { + LOGIN_ON_NEW_DEVICE = "login.start", + RECIPROCATE_LOGIN_ON_EXISTING_DEVICE = "login.reciprocate", +} + +export interface RendezvousCode { + intent: RendezvousIntent; + rendezvous?: { + transport: RendezvousTransportDetails; + algorithm: SecureRendezvousChannelAlgorithm; + }; +} diff --git a/src/rendezvous/error.ts b/src/rendezvous/error.ts new file mode 100644 index 00000000000..50a2bd3e4b0 --- /dev/null +++ b/src/rendezvous/error.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 "./cancellationReason"; + +export class RendezvousError extends Error { + constructor(message: string, public readonly code: RendezvousFailureReason) { + super(message); + } +} diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts new file mode 100644 index 00000000000..c3113d22c0c --- /dev/null +++ b/src/rendezvous/index.ts @@ -0,0 +1,84 @@ +/* +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 './cancellationReason'; +import { RendezvousChannel } from './channel'; +import { RendezvousCode, RendezvousIntent } from './code'; +import { RendezvousError } from './error'; +import { SimpleHttpRendezvousTransport, SimpleHttpRendezvousTransportDetails } from './transports'; +import { decodeBase64 } from '../crypto/olmlib'; +import { ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; +import { logger } from '../logger'; + +export * from './code'; +export * from './cancellationReason'; +export * from './transport'; +export * from './channel'; + +/** + * Attempts to parse the given code as a rendezvous and return a channel and transport. + * @param code The code to parse. + * @param onCancelled the cancellation listener to use for the transport and secure channel. + * @returns The channel and intent of the generatoer + */ +export async function buildChannelFromCode( + code: string, + onCancelled: RendezvousFailureListener, +): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { + let parsed: RendezvousCode; + try { + parsed = JSON.parse(code) as RendezvousCode; + } catch (err) { + throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); + } + + const { intent, rendezvous } = parsed; + + if (rendezvous?.transport.type !== 'http.v1') { + throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedTransport); + } + + const transportDetails = rendezvous.transport as SimpleHttpRendezvousTransportDetails; + + if (typeof transportDetails.uri !== 'string') { + throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); + } + + if (!intent || !Object.values(RendezvousIntent).includes(intent)) { + throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); + } + + const transport = new SimpleHttpRendezvousTransport( + onCancelled, + undefined, // client + undefined, // hsUrl + undefined, // fallbackRzServer + transportDetails.uri); + + if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { + throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); + } + + const ecdhCode = parsed as ECDHv1RendezvousCode; + + const theirPublicKey = decodeBase64(ecdhCode.rendezvous.key); + + logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); + return { + channel: new ECDHv1RendezvousChannel(transport, theirPublicKey), + intent, + }; +} diff --git a/src/rendezvous/transport.ts b/src/rendezvous/transport.ts new file mode 100644 index 00000000000..a28131dc372 --- /dev/null +++ b/src/rendezvous/transport.ts @@ -0,0 +1,59 @@ +/* +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 "./cancellationReason"; + +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. + */ + 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 contentType the content type of the data being sent + * @param data the data itself + */ + send(contentType: string, data: any): Promise; + + /** + * Receive data from the transport. + */ + receive(): Promise; + + /** + * 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/src/rendezvous/transports/index.ts b/src/rendezvous/transports/index.ts new file mode 100644 index 00000000000..8e7cadab1bc --- /dev/null +++ b/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 './simpleHttpTransport'; diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts new file mode 100644 index 00000000000..9d1ec565c10 --- /dev/null +++ b/src/rendezvous/transports/simpleHttpTransport.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 { logger } from '../../logger'; +import { sleep } from '../../utils'; +import { RendezvousFailureListener, RendezvousFailureReason } from '../cancellationReason'; +import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; +import { MatrixClient } from '../../matrix'; +import { PREFIX_UNSTABLE } from '../../http-api'; + +export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { + type: 'http.v1'; + uri: string; +} + +/** + * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) + * simple HTTP rendezvous protocol. + */ +export class SimpleHttpRendezvousTransport implements RendezvousTransport { + ready = false; + cancelled = false; + private uri?: string; + private etag?: string; + private expiresAt?: Date; + + private static fetch(resource: URL | string, options?: RequestInit): ReturnType { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + private static fetchFn?: typeof global.fetch; + + public static setFetchFn(fetchFn: typeof global.fetch): void { + SimpleHttpRendezvousTransport.fetchFn = fetchFn; + } + + constructor( + public onFailure?: RendezvousFailureListener, + private client?: MatrixClient, + private hsUrl?: string, + private fallbackRzServer?: string, + rendezvousUri?: string, + ) { + this.uri = rendezvousUri; + this.ready = !!this.uri; + } + + async details(): Promise { + if (!this.uri) { + throw new Error('Rendezvous not set up'); + } + + return { + type: 'http.v1', + uri: this.uri, + }; + } + + private async getPostEndpoint(): Promise { + if (!this.client && this.hsUrl) { + this.client = new MatrixClient({ + baseUrl: this.hsUrl, + }); + } + + if (this.client) { + try { + if (await this.client.doesServerSupportUnstableFeature('org.matrix.msc3886')) { + return `${this.client.baseUrl}${PREFIX_UNSTABLE}/org.matrix.msc3886/rendezvous`; + } + } catch (err) { + logger.warn('Failed to get unstable features', err); + } + } + + return this.fallbackRzServer; + } + + async send(contentType: string, data: any) { + 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'); + } + + logger.debug(`Sending data: ${data} to ${uri}`); + + const headers: Record = { 'content-type': contentType }; + if (this.etag) { + headers['if-match'] = this.etag; + } + + const res = await SimpleHttpRendezvousTransport.fetch(uri, { method, + headers, + body: data, + }); + if (res.status === 404) { + return this.cancel(RendezvousFailureReason.Unknown); + } + this.etag = res.headers.get("etag") ?? undefined; + + logger.debug(`Posted data to ${uri} new etag ${this.etag}`); + + 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); + } + // resolve location header which could be relative or absolute + this.uri = new URL(location, `${res.url}${res.url.endsWith('/') ? '' : '/'}`).href; + this.ready =true; + } + } + + async receive(): Promise { + if (!this.uri) { + throw new Error('Rendezvous not set up'); + } + // eslint-disable-next-line no-constant-condition + while (true) { + if (this.cancelled) { + return; + } + logger.debug(`Polling: ${this.uri} after etag ${this.etag}`); + const headers: Record = {}; + if (this.etag) { + headers['if-none-match'] = this.etag; + } + const poll = await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "GET", headers }); + + logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); + if (poll.status === 404) { + return this.cancel(RendezvousFailureReason.Unknown); + } + + // 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; + const data = await poll.json(); + logger.debug(`Received data: ${JSON.stringify(data)} from ${this.uri} with etag ${this.etag}`); + return data; + } + await sleep(1000); + } + } + + async cancel(reason: RendezvousFailureReason) { + 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 { + logger.debug(`Deleting channel: ${this.uri}`); + await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "DELETE" }); + } catch (e) { + logger.warn(e); + } + } + } +} From 7462dd147c2d7c2ebe84e403d2eb2181f4982863 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 13:22:32 +0100 Subject: [PATCH 16/74] Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better --- src/rendezvous/index.ts | 9 ++--- .../transports/simpleHttpTransport.ts | 33 ++++++++++++++----- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index c3113d22c0c..2a741243ac5 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -36,7 +36,7 @@ export * from './channel'; */ export async function buildChannelFromCode( code: string, - onCancelled: RendezvousFailureListener, + onFailure: RendezvousFailureListener, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -61,12 +61,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport( - onCancelled, - undefined, // client - undefined, // hsUrl - undefined, // fallbackRzServer - transportDetails.uri); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 9d1ec565c10..14cd9e5e44e 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -36,6 +36,10 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private uri?: string; private etag?: string; private expiresAt?: Date; + public onFailure?: RendezvousFailureListener; + private client?: MatrixClient; + private hsUrl?: string; + private fallbackRzServer?: string; private static fetch(resource: URL | string, options?: RequestInit): ReturnType { if (this.fetchFn) { @@ -50,13 +54,23 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { SimpleHttpRendezvousTransport.fetchFn = fetchFn; } - constructor( - public onFailure?: RendezvousFailureListener, - private client?: MatrixClient, - private hsUrl?: string, - private fallbackRzServer?: string, - rendezvousUri?: string, - ) { + constructor({ + onFailure, + client, + hsUrl, + fallbackRzServer, + rendezvousUri, + }: { + onFailure?: RendezvousFailureListener; + client?: MatrixClient; + hsUrl?: string; + fallbackRzServer?: string; + rendezvousUri?: string; + }) { + this.onFailure = onFailure; + this.client = client; + this.hsUrl = hsUrl; + this.fallbackRzServer = fallbackRzServer; this.uri = rendezvousUri; this.ready = !!this.uri; } @@ -130,8 +144,11 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { 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, `${res.url}${res.url.endsWith('/') ? '' : '/'}`).href; + this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}`).href; this.ready =true; } } From 9eed4938a5dc476deb35445ce33d8a591fff10d3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 13:22:49 +0100 Subject: [PATCH 17/74] Start of some unit tests --- spec/unit/rendezvous/rendezvous.spec.ts | 128 ++++++++ .../rendezvous/simpleHttpTransport.spec.ts | 287 ++++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100644 spec/unit/rendezvous/rendezvous.spec.ts create mode 100644 spec/unit/rendezvous/simpleHttpTransport.spec.ts diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts new file mode 100644 index 00000000000..d948731d482 --- /dev/null +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -0,0 +1,128 @@ +/* +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 MockHttpBackend from "matrix-mock-request"; + +import '../../olm-loader'; +import { buildChannelFromCode } from "../../../src/rendezvous"; +import { SimpleHttpRendezvousTransport } from '../../../src/rendezvous/transports'; + +describe("Rendezvous", function() { + beforeAll(async function() { + await global.Olm.init(); + }); + + const getHttpBackend = (): MockHttpBackend => { + const httpBackend = new MockHttpBackend(); + SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); + return httpBackend; + }; + + describe("buildChannelFromCode", function() { + it("non-JSON", function() { + expect(buildChannelFromCode("xyz", () => {})).rejects.toThrow("Invalid code"); + }); + + it("invalid JSON", function() { + expect(buildChannelFromCode(JSON.stringify({}), () => {})).rejects.toThrow("Unsupported transport"); + }); + + it("invalid transport type", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "foo" } }, + }), () => {})).rejects.toThrow("Unsupported transport"); + }); + + it("missing URI", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1" } }, + }), () => {})).rejects.toThrow("Invalid code"); + }); + + it("invalid URI field", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1", uri: false } }, + }), () => {})).rejects.toThrow("Invalid code"); + }); + + it("missing intent", function() { + expect(buildChannelFromCode(JSON.stringify({ + rendezvous: { transport: { type: "http.v1", uri: "something" } }, + }), () => {})).rejects.toThrow("Invalid intent"); + }); + + it("invalid intent", function() { + expect(buildChannelFromCode(JSON.stringify({ + intent: 'asd', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {})).rejects.toThrow("Invalid intent"); + }); + + it("login.reciprocate", async function() { + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.reciprocate', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {}); + expect(x.intent).toBe("login.reciprocate"); + }); + + it("login.start", async function() { + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.start', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "something" }, + }, + }), () => {}); + expect(x.intent).toBe("login.start"); + }); + + it("parse and get", async function() { + const httpBackend = getHttpBackend(); + const x = await buildChannelFromCode(JSON.stringify({ + intent: 'login.start', + rendezvous: { + algorithm: "m.rendezvous.v1.curve25519-aes-sha256", + key: "", + transport: { type: "http.v1", uri: "https://rz.server/123456" }, + }, + }), () => {}); + expect(x.intent).toBe("login.start"); + + const prom = x.channel.receive(); + httpBackend.when("GET", "https://rz.server/123456").response = { + body: {}, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual({}); + }); + }); +}); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts new file mode 100644 index 00000000000..a3a63c7f427 --- /dev/null +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -0,0 +1,287 @@ +/* +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 MockHttpBackend from "matrix-mock-request"; + +import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; + +describe("SimpleHttpRendezvousTransport", function() { + const getHttpBackend = (): MockHttpBackend => { + const httpBackend = new MockHttpBackend(); + SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); + return httpBackend; + }; + + async function postAndCheckLocation( + fallbackRzServer: string, + locationResponse: string, + expectedFinalLocation: string, + ) { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", fallbackRzServer).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: locationResponse, + }, + }, + }; + await httpBackend.flush(''); + await prom; + } + { // first GET without etag + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", expectedFinalLocation).response = { + body: {}, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({}); + httpBackend.verifyNoOutstandingRequests(); + httpBackend.verifyNoOutstandingExpectation(); + } + } + it("should throw an error when no server available", function() { + getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({}); + expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); + }); + + it("POST to fallback server", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + }); + + it("POST with absolute path response", async function() { + await postAndCheckLocation("https://fallbackserver/rz", "/123", "https://fallbackserver/123"); + }); + + it("POST with relative path response", async function() { + await postAndCheckLocation("https://fallbackserver/rz", "123", "https://fallbackserver/rz/123"); + }); + + it("POST with relative path response including parent", async function() { + await postAndCheckLocation("https://fallbackserver/rz/abc", "../xyz/123", "https://fallbackserver/rz/xyz/123"); + }); + + it("POST to follow 307 to other server", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + const prom = simpleHttpTransport.send("application/json", {}); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 307, + headers: { + location: "https://redirected.fallbackserver/rz", + }, + }, + }; + httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://redirected.fallbackserver/rz/123", + etag: "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + }); + + it("POST and GET", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { + expect(headers["content-type"]).toEqual("application/json"); + expect(data).toEqual({ foo: "baa" }); + }).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + } + { // first GET without etag + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://fallbackserver/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + { // subsequent GET which should have etag from previous request + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers, data }) => { + expect(headers["if-none-match"]).toEqual("aaa"); + }).response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "bbb", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); + + it("POST and PUTs", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + }); + { // initial POST + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { + expect(headers["content-type"]).toEqual("application/json"); + expect(data).toEqual({ foo: "baa" }); + }).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + { // first PUT without etag + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ a: "b" })); + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + expect(headers["if-match"]).toBeUndefined(); + }).response = { + body: null, + response: { + statusCode: 202, + headers: { + "etag": "aaa", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + { // subsequent PUT which should have etag from previous request + const prom = simpleHttpTransport.receive(); + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + expect(headers["if-match"]).toEqual("aaa"); + }).response = { + body: null, + response: { + statusCode: 202, + headers: { + "etag": "bbb", + }, + }, + }; + await httpBackend.flush('', 1); + await prom; + } + }); + + it("init with URI", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + rendezvousUri: "https://server/rz/123", + }); + { + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://server/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); + + it("init from HS", async function() { + const httpBackend = getHttpBackend(); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + rendezvousUri: "https://server/rz/123", + }); + { + const prom = simpleHttpTransport.receive(); + httpBackend.when("GET", "https://server/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + "etag": "aaa", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toEqual({ foo: "baa" }); + } + }); +}); From b04bc704fbc491f0fbc149fc005eb6f60a3e2318 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:08:55 +0100 Subject: [PATCH 18/74] Make AES work on Node.js as well as browser --- src/rendezvous/channels/ecdhV1.ts | 52 ++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 99bcefded56..d875c51a985 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -28,6 +28,7 @@ import { } from '../index'; import { SecureRendezvousChannelAlgorithm } from '.'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; +import { getCrypto } from '../../utils'; const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? (window.crypto.subtle || window.crypto.webkitSubtle) : null; @@ -60,9 +61,13 @@ function generateDecimalSas(sasBytes: number[]): string { return digits.join('-'); } -async function importKey(key: Uint8Array): Promise { +async function importKey(key: Uint8Array): Promise { + if (getCrypto()) { + return key; + } + if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); + throw new Error('Neither Web Crypto nor Node.js crypto available'); } const imported = subtleCrypto.importKey( @@ -83,7 +88,7 @@ async function importKey(key: Uint8Array): Promise { export class ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; - private aesKey?: CryptoKey; + private aesKey?: CryptoKey | Uint8Array; public onFailure?: (reason: RendezvousFailureReason) => void; constructor( @@ -172,8 +177,21 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } private async encrypt(data: any): Promise { - if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); + if (this.aesKey instanceof Uint8Array) { + const crypto = getCrypto(); + + const iv = crypto.randomBytes(32); + const cipher = crypto.createCipheriv("aes-256-gcm", this.aesKey as Uint8Array, iv, { authTagLength: 16 }); + const ciphertext = Buffer.concat([ + cipher.update(data, "utf8"), + cipher.final(), + cipher.getAuthTag(), + ]); + + return JSON.stringify({ + iv: encodeBase64(iv), + ciphertext: encodeBase64(ciphertext), + }); } const iv = new Uint8Array(32); @@ -185,8 +203,9 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { { name: "AES-GCM", iv, + tagLength: 128, }, - this.aesKey, + this.aesKey as CryptoKey, encodedData, ); @@ -212,22 +231,31 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } private async decrypt({ iv, ciphertext }: { iv: string, ciphertext: string }): Promise { - if (!subtleCrypto) { - throw new Error('Subtle crypto not available'); - } - if (!ciphertext || !iv) { throw new Error('Missing ciphertext and/or iv'); } const ciphertextBytes = decodeBase64(ciphertext); + if (this.aesKey instanceof Uint8Array) { + const crypto = getCrypto(); + // in contrast to Web Crypto API, Node's crypto needs the auth tag split off the cipher text + const ciphertextOnly = ciphertextBytes.slice(0, ciphertextBytes.length - 16); + const authTag = ciphertextBytes.slice(ciphertextBytes.length - 16); + const decipher = crypto.createDecipheriv( + "aes-256-gcm", this.aesKey as Uint8Array, decodeBase64(iv), { authTagLength: 16 }, + ); + decipher.setAuthTag(authTag); + return decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"); + } + const plaintext = await subtleCrypto.decrypt( { name: "AES-GCM", iv: decodeBase64(iv), + tagLength: 128, }, - this.aesKey, + this.aesKey as CryptoKey, ciphertextBytes, ); @@ -251,7 +279,7 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { } const decrypted = await this.decrypt(data); logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); - return decrypted; + return JSON.parse(decrypted); } else if (this.aesKey) { throw new Error('Data received but no ciphertext'); } From 18f7383ee4c768e14bc319dd7aa31bee8742b3ef Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:09:05 +0100 Subject: [PATCH 19/74] Tests for ECDH/X25519 --- spec/unit/rendezvous/ecdh.spec.ts | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spec/unit/rendezvous/ecdh.spec.ts diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts new file mode 100644 index 00000000000..61b434ad597 --- /dev/null +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -0,0 +1,123 @@ +/* +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 crypto from 'crypto'; + +import '../../olm-loader'; +import { + RendezvousFailureListener, + RendezvousFailureReason, + RendezvousIntent, + RendezvousTransport, + RendezvousTransportDetails, +} from "../../../src/rendezvous"; +import { ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; +import { decodeBase64 } from '../../../src/crypto/olmlib'; +import { setCrypto, sleep } from '../../../src/utils'; + +class DummyTransport implements RendezvousTransport { + otherParty?: DummyTransport; + etag?: string; + data = null; + + ready = false; + + onCancelled?: RendezvousFailureListener; + + details(): Promise { + return Promise.resolve({ + type: 'dummy', + }); + } + + async send(contentType: string, data: any): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag === this.etag) { + this.data = data; + this.etag = Math.random().toString(); + return; + } + await sleep(100); + } + } + + async receive(): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag !== this.etag) { + this.etag = this.otherParty?.etag; + return JSON.parse(this.otherParty.data); + } + await sleep(100); + } + } + + cancel(reason: RendezvousFailureReason): Promise { + throw new Error("Method not implemented."); + } +} + +describe("ECDHv1", function() { + beforeAll(async function() { + setCrypto(crypto); + await global.Olm.init(); + }); + + it("initiator wants to sign in", async function() { + const aliceTransport = new DummyTransport(); + const bobTransport = new DummyTransport(); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = "hello world"; + await alice.send(message); + const bobReceive = await bob.receive(); + expect(bobReceive).toEqual(message); + }); + + it("initiator wants to reciprocate", async function() { + const aliceTransport = new DummyTransport(); + const bobTransport = new DummyTransport(); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + const message = "hello world"; + await bob.send(message); + const aliceReceive = await alice.receive(); + expect(aliceReceive).toEqual(message); + }); +}); From c1266801e1709df6b22d2ec90ae176855fa317af Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:18:32 +0100 Subject: [PATCH 20/74] stric mode linting --- spec/unit/rendezvous/ecdh.spec.ts | 2 +- src/rendezvous/channels/ecdhV1.ts | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 61b434ad597..223a23577c5 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -60,7 +60,7 @@ class DummyTransport implements RendezvousTransport { while (true) { if (!this.etag || this.otherParty?.etag !== this.etag) { this.etag = this.otherParty?.etag; - return JSON.parse(this.otherParty.data); + return this.otherParty?.data ? JSON.parse(this.otherParty.data) : undefined; } await sleep(100); } diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index d875c51a985..f116717d661 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -67,7 +67,7 @@ async function importKey(key: Uint8Array): Promise { } if (!subtleCrypto) { - throw new Error('Neither Web Crypto nor Node.js crypto available'); + throw new Error('Neither Web Crypto nor Node.js crypto are available'); } const imported = subtleCrypto.importKey( @@ -104,10 +104,6 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Code already generated'); } - const data = { - "algorithm": SecureRendezvousChannelAlgorithm.ECDH_V1, - }; - await this.send({ algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1 }); const rendezvous: ECDHv1RendezvousCode = { @@ -115,7 +111,6 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, key: encodeBase64(this.ourPublicKey), transport: await this.transport.details(), - ...data, }, intent, }; @@ -155,10 +150,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { }); } - this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey)); + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); - const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; - const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; + const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; let aesInfo = SecureRendezvousChannelAlgorithm.ECDH_V1.toString(); aesInfo += `|${encodeBase64(initiatorKey)}`; aesInfo += `|${encodeBase64(recipientKey)}`; @@ -168,7 +163,7 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { this.aesKey = await importKey(aesKeyBytes); logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); - logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey)}`); + logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey!)}`); logger.debug(`AES info: ${aesInfo}`); logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); @@ -194,6 +189,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { }); } + if (!subtleCrypto) { + throw new Error('Neither Web Crypto nor Node.js crypto are available'); + } + const iv = new Uint8Array(32); window.crypto.getRandomValues(iv); @@ -249,6 +248,10 @@ export class ECDHv1RendezvousChannel implements RendezvousChannel { return decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"); } + if (!subtleCrypto) { + throw new Error('Neither Web Crypto nor Node.js crypto are available'); + } + const plaintext = await subtleCrypto.decrypt( { name: "AES-GCM", From 145aaa0e9e3e2e609f097e1a1c945f684f9ad8c4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 18:21:57 +0100 Subject: [PATCH 21/74] Fix incorrect test --- spec/unit/rendezvous/simpleHttpTransport.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a3a63c7f427..a6d344cad54 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -209,8 +209,9 @@ describe("SimpleHttpRendezvousTransport", function() { } { // first PUT without etag const prom = simpleHttpTransport.send("application/json", JSON.stringify({ a: "b" })); - httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { + httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => { expect(headers["if-match"]).toBeUndefined(); + expect(data).toEqual({ a: "b" }); }).response = { body: null, response: { @@ -224,7 +225,7 @@ describe("SimpleHttpRendezvousTransport", function() { await prom; } { // subsequent PUT which should have etag from previous request - const prom = simpleHttpTransport.receive(); + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ c: "d" })); httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { expect(headers["if-match"]).toEqual("aaa"); }).response = { From 14e4a763fc890bb5461a1262952e6fa0f564494a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 21:42:05 +0100 Subject: [PATCH 22/74] Refactor full rendezvous logic out of react-sdk into js-sdk --- src/rendezvous/index.ts | 1 + src/rendezvous/rendezvous.ts | 362 +++++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/rendezvous/rendezvous.ts diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 2a741243ac5..cd0fd673c12 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -27,6 +27,7 @@ export * from './code'; export * from './cancellationReason'; export * from './transport'; export * from './channel'; +export * from './rendezvous'; /** * Attempts to parse the given code as a rendezvous and return a channel and transport. diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts new file mode 100644 index 00000000000..65b16192835 --- /dev/null +++ b/src/rendezvous/rendezvous.ts @@ -0,0 +1,362 @@ +/* +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 { RendezvousChannel } from "."; +import { LoginTokenPostResponse } from "../@types/auth"; +import { MatrixClient } from "../client"; +import { CrossSigningInfo, requestKeysDuringVerification } from "../crypto/CrossSigning"; +import { DeviceInfo } from "../crypto/deviceinfo"; +import { IAuthData } from "../interactive-auth"; +import { logger } from "../logger"; +import { createClient } from "../matrix"; +import { sleep } from "../utils"; +import { RendezvousFailureReason } from "./cancellationReason"; +import { RendezvousIntent } from "./code"; + +export enum PayloadType { + Start = 'm.login.start', + Finish = 'm.login.finish', + Progress = 'm.login.progress', +} + +export class Rendezvous { + private cli?: MatrixClient; + private newDeviceId?: string; + private newDeviceKey?: string; + private ourIntent: RendezvousIntent; + public code?: string; + public onFailure?: (reason: RendezvousFailureReason) => void; + + constructor(public channel: RendezvousChannel, cli?: MatrixClient) { + this.cli = cli; + this.ourIntent = this.isNewDevice ? + RendezvousIntent.LOGIN_ON_NEW_DEVICE : + RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; + } + + async generateCode(): Promise { + if (this.code) { + return; + } + + this.code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + + private get isNewDevice(): boolean { + return !this.cli; + } + + private async areIntentsIncompatible(theirIntent: RendezvousIntent): Promise { + const incompatible = theirIntent === this.ourIntent; + + logger.info(`ourIntent: ${this.ourIntent}, theirIntent: ${theirIntent}, incompatible: ${incompatible}`); + + if (incompatible) { + await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); + await this.channel.cancel( + this.isNewDevice ? + RendezvousFailureReason.OtherDeviceNotSignedIn : + RendezvousFailureReason.OtherDeviceAlreadySignedIn, + ); + } + + return incompatible; + } + + async startAfterShowingCode(): Promise { + return this.start(); + } + + async startAfterScanningCode(theirIntent: RendezvousIntent): Promise { + return this.start(theirIntent); + } + + private async start(theirIntent?: RendezvousIntent): Promise { + const didScan = !!theirIntent; + + const checksum = await this.channel.connect(); + + logger.info(`Connected to secure channel with checksum: ${checksum}`); + + if (didScan) { + if (await this.areIntentsIncompatible(theirIntent)) { + // a m.login.finish event is sent as part of areIntentsIncompatible + return undefined; + } + } + + if (this.cli) { + if (didScan) { + await this.channel.receive(); // wait for ack + } + + // determine available protocols + if (!(await this.cli.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { + logger.info("Server doesn't support MSC3882"); + await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); + return undefined; + } + + await this.send({ type: PayloadType.Progress, protocols: ['login_token'] }); + + logger.info('Waiting for other device to chose protocol'); + const { type, protocol, outcome } = await this.channel.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') { + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + } else { + if (!didScan) { + logger.info("Sending ack"); + await this.send({ type: PayloadType.Progress }); + } + + logger.info("Waiting for protocols"); + const { protocols } = await this.channel.receive(); + + if (!Array.isArray(protocols) || !protocols.includes('login_token')) { + await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + + await this.send({ type: PayloadType.Progress, protocol: "login_token" }); + } + + return checksum; + } + + async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { + await this.channel.send({ type, ...payload }); + } + + async completeLoginOnNewDevice(): Promise<{ + userId: string; + deviceId: string; + accessToken: string; + homeserverUrl: string; + } | undefined> { + logger.info('Waiting for login_token'); + + // eslint-disable-next-line camelcase + const { type, login_token: token, outcome, homeserver } = await this.channel.receive(); + + if (type === PayloadType.Finish) { + switch (outcome ?? '') { + case 'unsupported': + await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); + break; + default: + await this.cancel(RendezvousFailureReason.Unknown); + } + return undefined; + } + + if (!homeserver) { + throw new Error("No homeserver returned"); + } + // eslint-disable-next-line camelcase + if (!token) { + throw new Error("No login token returned"); + } + + const client = createClient({ + baseUrl: homeserver, + }); + + const { device_id: deviceId, user_id: userId, access_token: accessToken } = + await client.login("m.login.token", { token }); + + return { + userId, + deviceId, + accessToken, + homeserverUrl: homeserver, + }; + } + + async completeVerificationOnNewDevice(client: MatrixClient): Promise { + await this.send({ + type: PayloadType.Progress, + outcome: 'success', + device_id: client.getDeviceId(), + device_key: client.getDeviceEd25519Key(), + }); + + // await confirmation of verification + const { + verifying_device_id: verifyingDeviceId, + master_key: masterKey, + verifying_device_key: verifyingDeviceKey, + } = await this.channel.receive(); + + const userId = client.getUserId()!; + const verifyingDeviceFromServer = + client.crypto.deviceList.getStoredDevice(userId, verifyingDeviceId); + + if (verifyingDeviceFromServer?.getFingerprint() === verifyingDeviceKey) { + // set other device as verified + logger.info(`Setting device ${verifyingDeviceId} as verified`); + await client.setDeviceVerified(userId, verifyingDeviceId, true); + + if (masterKey) { + // set master key as trusted + await client.setDeviceVerified(userId, masterKey, true); + } + + // request secrets from the verifying device + logger.info(`Requesting secrets from ${verifyingDeviceId}`); + await requestKeysDuringVerification(client, userId, verifyingDeviceId); + } else { + logger.info(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + } + } + + async declineLoginOnExistingDevice() { + logger.info('User declined linking'); + await this.send({ type: PayloadType.Finish, outcome: 'declined' }); + } + + async confirmLoginOnExistingDevice(): Promise { + const client = this.cli; + + logger.info("Requesting login token"); + + const loginTokenResponse = await client.requestLoginToken(); + + if (typeof (loginTokenResponse as IAuthData).session === 'string') { + // TODO: handle UIA response + throw new Error("UIA isn't supported yet"); + } + // eslint-disable-next-line camelcase + const { login_token } = loginTokenResponse as LoginTokenPostResponse; + + // eslint-disable-next-line camelcase + await this.send({ type: PayloadType.Progress, login_token, homeserver: client.baseUrl }); + + logger.info('Waiting for outcome'); + const res = await this.channel.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 checkAndCrossSignDevice(deviceInfo: DeviceInfo) { + // 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()}`, + ); + } + + // mark the device as verified locally + cross sign + logger.info(`Marking device ${this.newDeviceId} as verified`); + const info = await this.cli.crypto.setDeviceVerification( + this.cli.getUserId(), + this.newDeviceId, + true, false, true, + ); + + const masterPublicKey = this.cli.crypto.crossSigningInfo.getId('master'); + + await this.send({ + type: PayloadType.Finish, + outcome: 'verified', + verifying_device_id: this.cli.getDeviceId(), + verifying_device_key: this.cli.getDeviceEd25519Key(), + master_key: masterPublicKey, + }); + + return info; + } + + async crossSign(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; + } + + const cli = this.cli; + + { + const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + + if (deviceInfo) { + return await this.checkAndCrossSignDevice(deviceInfo); + } + } + + logger.info("New device is not online"); + await sleep(timeout); + + logger.info("Going to wait for new device to be online"); + + { + const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + + if (deviceInfo) { + return await this.checkAndCrossSignDevice(deviceInfo); + } + } + + throw new Error('Device not online within timeout'); + } + + async userCancelled(): Promise { + this.cancel(RendezvousFailureReason.UserCancelled); + } + + async cancel(reason: RendezvousFailureReason) { + await this.channel.cancel(reason); + } + + async close() { + await this.channel.close(); + } +} From adb0a5efe56623250d9ff5fa1065dd6fbc00d3d2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 22:48:31 +0100 Subject: [PATCH 23/74] Use correct unstable import --- src/rendezvous/transports/simpleHttpTransport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 14cd9e5e44e..a8a019d7701 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -19,7 +19,7 @@ import { sleep } from '../../utils'; import { RendezvousFailureListener, RendezvousFailureReason } from '../cancellationReason'; import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; -import { PREFIX_UNSTABLE } from '../../http-api'; +import { ClientPrefix } from '../../http-api'; export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { type: 'http.v1'; @@ -96,7 +96,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.client) { try { if (await this.client.doesServerSupportUnstableFeature('org.matrix.msc3886')) { - return `${this.client.baseUrl}${PREFIX_UNSTABLE}/org.matrix.msc3886/rendezvous`; + return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; } } catch (err) { logger.warn('Failed to get unstable features', err); From 707f48624fd7bf221fc6bf14aaaaabade354157b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 12 Oct 2022 23:12:42 +0100 Subject: [PATCH 24/74] Pass fetch around --- spec/unit/rendezvous/rendezvous.spec.ts | 24 +++++++++---------- .../rendezvous/simpleHttpTransport.spec.ts | 13 +++++++--- src/rendezvous/index.ts | 3 ++- .../transports/simpleHttpTransport.ts | 23 ++++++------------ 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index d948731d482..a87339ba063 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; import { buildChannelFromCode } from "../../../src/rendezvous"; -import { SimpleHttpRendezvousTransport } from '../../../src/rendezvous/transports'; describe("Rendezvous", function() { beforeAll(async function() { @@ -27,41 +26,42 @@ describe("Rendezvous", function() { const getHttpBackend = (): MockHttpBackend => { const httpBackend = new MockHttpBackend(); - SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); return httpBackend; }; + const fetch = getHttpBackend().fetchFn as typeof global.fetch; + describe("buildChannelFromCode", function() { it("non-JSON", function() { - expect(buildChannelFromCode("xyz", () => {})).rejects.toThrow("Invalid code"); + expect(buildChannelFromCode("xyz", () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("invalid JSON", function() { - expect(buildChannelFromCode(JSON.stringify({}), () => {})).rejects.toThrow("Unsupported transport"); + expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetch)).rejects.toThrow("Unsupported transport"); }); it("invalid transport type", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "foo" } }, - }), () => {})).rejects.toThrow("Unsupported transport"); + }), () => {}, fetch)).rejects.toThrow("Unsupported transport"); }); it("missing URI", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1" } }, - }), () => {})).rejects.toThrow("Invalid code"); + }), () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("invalid URI field", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: false } }, - }), () => {})).rejects.toThrow("Invalid code"); + }), () => {}, fetch)).rejects.toThrow("Invalid code"); }); it("missing intent", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: "something" } }, - }), () => {})).rejects.toThrow("Invalid intent"); + }), () => {}, fetch)).rejects.toThrow("Invalid intent"); }); it("invalid intent", function() { @@ -72,7 +72,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {})).rejects.toThrow("Invalid intent"); + }), () => {}, fetch)).rejects.toThrow("Invalid intent"); }); it("login.reciprocate", async function() { @@ -83,7 +83,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.reciprocate"); }); @@ -95,7 +95,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.start"); }); @@ -108,7 +108,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "https://rz.server/123456" }, }, - }), () => {}); + }), () => {}, fetch); expect(x.intent).toBe("login.start"); const prom = x.channel.receive(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a6d344cad54..9c97e14d95c 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -21,17 +21,18 @@ import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transport describe("SimpleHttpRendezvousTransport", function() { const getHttpBackend = (): MockHttpBackend => { const httpBackend = new MockHttpBackend(); - SimpleHttpRendezvousTransport.setFetchFn(httpBackend.fetchFn as typeof global.fetch); return httpBackend; }; + const fetch = getHttpBackend().fetchFn as typeof global.fetch; + async function postAndCheckLocation( fallbackRzServer: string, locationResponse: string, expectedFinalLocation: string, ) { const httpBackend = getHttpBackend(); - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,7 +66,7 @@ describe("SimpleHttpRendezvousTransport", function() { } it("should throw an error when no server available", function() { getHttpBackend(); - const simpleHttpTransport = new SimpleHttpRendezvousTransport({}); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); @@ -73,6 +74,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -104,6 +106,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -133,6 +136,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -189,6 +193,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", + fetch, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -246,6 +251,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", + fetch, }); { const prom = simpleHttpTransport.receive(); @@ -268,6 +274,7 @@ describe("SimpleHttpRendezvousTransport", function() { const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", + fetch, }); { const prom = simpleHttpTransport.receive(); diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index cd0fd673c12..a12392a2284 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -38,6 +38,7 @@ export * from './rendezvous'; export async function buildChannelFromCode( code: string, onFailure: RendezvousFailureListener, + fetch: typeof global.fetch, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -62,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri }); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetch }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index a8a019d7701..99b7c7d8cb9 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,19 +40,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - - private static fetch(resource: URL | string, options?: RequestInit): ReturnType { - if (this.fetchFn) { - return this.fetchFn(resource, options); - } - return global.fetch(resource, options); - } - - private static fetchFn?: typeof global.fetch; - - public static setFetchFn(fetchFn: typeof global.fetch): void { - SimpleHttpRendezvousTransport.fetchFn = fetchFn; - } + private fetch: typeof global.fetch; constructor({ onFailure, @@ -60,13 +48,16 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { hsUrl, fallbackRzServer, rendezvousUri, + fetch, }: { + fetch: typeof global.fetch; onFailure?: RendezvousFailureListener; client?: MatrixClient; hsUrl?: string; fallbackRzServer?: string; rendezvousUri?: string; }) { + this.fetch = fetch; this.onFailure = onFailure; this.client = client; this.hsUrl = hsUrl; @@ -124,7 +115,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { headers['if-match'] = this.etag; } - const res = await SimpleHttpRendezvousTransport.fetch(uri, { method, + const res = await this.fetch(uri, { method, headers, body: data, }); @@ -167,7 +158,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.etag) { headers['if-none-match'] = this.etag; } - const poll = await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "GET", headers }); + const poll = await this.fetch(this.uri, { method: "GET", headers }); logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); if (poll.status === 404) { @@ -201,7 +192,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { if (this.uri && reason === RendezvousFailureReason.UserDeclined) { try { logger.debug(`Deleting channel: ${this.uri}`); - await SimpleHttpRendezvousTransport.fetch(this.uri, { method: "DELETE" }); + await this.fetch(this.uri, { method: "DELETE" }); } catch (e) { logger.warn(e); } From 4b724668ed0dd6be9084d73d51965f78e6a568ca Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 12:41:04 +0100 Subject: [PATCH 25/74] Make correct usage of fetch in tests --- spec/unit/rendezvous/rendezvous.spec.ts | 12 +++++------ .../rendezvous/simpleHttpTransport.spec.ts | 21 +++++++------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index a87339ba063..ea994d9c41b 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -24,12 +24,13 @@ describe("Rendezvous", function() { await global.Olm.init(); }); - const getHttpBackend = (): MockHttpBackend => { - const httpBackend = new MockHttpBackend(); - return httpBackend; - }; + let httpBackend: MockHttpBackend; + let fetch: typeof global.fetch; - const fetch = getHttpBackend().fetchFn as typeof global.fetch; + beforeEach(function() { + httpBackend = new MockHttpBackend(); + fetch = httpBackend.fetchFn as typeof global.fetch; + }); describe("buildChannelFromCode", function() { it("non-JSON", function() { @@ -100,7 +101,6 @@ describe("Rendezvous", function() { }); it("parse and get", async function() { - const httpBackend = getHttpBackend(); const x = await buildChannelFromCode(JSON.stringify({ intent: 'login.start', rendezvous: { diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 9c97e14d95c..a093b1e0c52 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -19,19 +19,19 @@ import MockHttpBackend from "matrix-mock-request"; import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; describe("SimpleHttpRendezvousTransport", function() { - const getHttpBackend = (): MockHttpBackend => { - const httpBackend = new MockHttpBackend(); - return httpBackend; - }; + let httpBackend: MockHttpBackend; + let fetch: typeof global.fetch; - const fetch = getHttpBackend().fetchFn as typeof global.fetch; + beforeEach(function() { + httpBackend = new MockHttpBackend(); + fetch = httpBackend.fetchFn as typeof global.fetch; + }); async function postAndCheckLocation( fallbackRzServer: string, locationResponse: string, expectedFinalLocation: string, ) { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); @@ -65,13 +65,11 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -103,7 +101,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST to follow 307 to other server", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -133,7 +130,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and GET", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -172,7 +168,7 @@ describe("SimpleHttpRendezvousTransport", function() { } { // subsequent GET which should have etag from previous request const prom = simpleHttpTransport.receive(); - httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers, data }) => { + httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => { expect(headers["if-none-match"]).toEqual("aaa"); }).response = { body: { foo: "baa" }, @@ -190,7 +186,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and PUTs", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetch, @@ -248,7 +243,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init with URI", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetch, @@ -271,7 +265,6 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init from HS", async function() { - const httpBackend = getHttpBackend(); const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetch, From ccf7b7303f6645d87cce11264d2db46a9c6a7de4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:05:11 +0100 Subject: [PATCH 26/74] fix: you can't call fetch when it's not on window --- spec/unit/rendezvous/rendezvous.spec.ts | 25 ++++++++++--------- .../rendezvous/simpleHttpTransport.spec.ts | 20 +++++++-------- src/rendezvous/index.ts | 4 +-- .../transports/simpleHttpTransport.ts | 15 ++++++++--- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index ea994d9c41b..6c1ecbd7c28 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -25,44 +25,45 @@ describe("Rendezvous", function() { }); let httpBackend: MockHttpBackend; - let fetch: typeof global.fetch; + let fetchFn: typeof global.fetchFn; beforeEach(function() { httpBackend = new MockHttpBackend(); - fetch = httpBackend.fetchFn as typeof global.fetch; + fetchFn = httpBackend.fetchFn as typeof global.fetch; }); describe("buildChannelFromCode", function() { it("non-JSON", function() { - expect(buildChannelFromCode("xyz", () => {}, fetch)).rejects.toThrow("Invalid code"); + expect(buildChannelFromCode("xyz", () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("invalid JSON", function() { - expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetch)).rejects.toThrow("Unsupported transport"); + expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetchFn)) + .rejects.toThrow("Unsupported transport"); }); it("invalid transport type", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "foo" } }, - }), () => {}, fetch)).rejects.toThrow("Unsupported transport"); + }), () => {}, fetchFn)).rejects.toThrow("Unsupported transport"); }); it("missing URI", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1" } }, - }), () => {}, fetch)).rejects.toThrow("Invalid code"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("invalid URI field", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: false } }, - }), () => {}, fetch)).rejects.toThrow("Invalid code"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); }); it("missing intent", function() { expect(buildChannelFromCode(JSON.stringify({ rendezvous: { transport: { type: "http.v1", uri: "something" } }, - }), () => {}, fetch)).rejects.toThrow("Invalid intent"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); }); it("invalid intent", function() { @@ -73,7 +74,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch)).rejects.toThrow("Invalid intent"); + }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); }); it("login.reciprocate", async function() { @@ -84,7 +85,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.reciprocate"); }); @@ -96,7 +97,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "something" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.start"); }); @@ -108,7 +109,7 @@ describe("Rendezvous", function() { key: "", transport: { type: "http.v1", uri: "https://rz.server/123456" }, }, - }), () => {}, fetch); + }), () => {}, fetchFn); expect(x.intent).toBe("login.start"); const prom = x.channel.receive(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index a093b1e0c52..2c5f6ee7dab 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -20,11 +20,11 @@ import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transport describe("SimpleHttpRendezvousTransport", function() { let httpBackend: MockHttpBackend; - let fetch: typeof global.fetch; + let fetchFn: typeof global.fetch; beforeEach(function() { httpBackend = new MockHttpBackend(); - fetch = httpBackend.fetchFn as typeof global.fetch; + fetchFn = httpBackend.fetchFn as typeof global.fetch; }); async function postAndCheckLocation( @@ -32,7 +32,7 @@ describe("SimpleHttpRendezvousTransport", function() { locationResponse: string, expectedFinalLocation: string, ) { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetch }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,14 +65,14 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetch }); + const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetchFn }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -103,7 +103,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST to follow 307 to other server", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", "https://fallbackserver/rz").response = { @@ -132,7 +132,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST and GET", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -188,7 +188,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("POST and PUTs", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", - fetch, + fetchFn, }); { // initial POST const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); @@ -245,7 +245,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("init with URI", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", - fetch, + fetchFn, }); { const prom = simpleHttpTransport.receive(); @@ -267,7 +267,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("init from HS", async function() { const simpleHttpTransport = new SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", - fetch, + fetchFn, }); { const prom = simpleHttpTransport.receive(); diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index a12392a2284..646b9dc3975 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -38,7 +38,7 @@ export * from './rendezvous'; export async function buildChannelFromCode( code: string, onFailure: RendezvousFailureListener, - fetch: typeof global.fetch, + fetchFn?: typeof global.fetch, ): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { let parsed: RendezvousCode; try { @@ -63,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetch }); + const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 99b7c7d8cb9..5816320c46a 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,7 +40,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - private fetch: typeof global.fetch; + private fetchFn: typeof global.fetch; constructor({ onFailure, @@ -48,16 +48,16 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { hsUrl, fallbackRzServer, rendezvousUri, - fetch, + fetchFn, }: { - fetch: typeof global.fetch; + fetchFn?: typeof global.fetch; onFailure?: RendezvousFailureListener; client?: MatrixClient; hsUrl?: string; fallbackRzServer?: string; rendezvousUri?: string; }) { - this.fetch = fetch; + this.fetchFn = fetchFn; this.onFailure = onFailure; this.client = client; this.hsUrl = hsUrl; @@ -77,6 +77,13 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { }; } + 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 { if (!this.client && this.hsUrl) { this.client = new MatrixClient({ From da2f024749202dc224df2243f63cfb0a57a24c2b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:16:19 +0100 Subject: [PATCH 27/74] Use class names to make it clearer that these are unstable MSC implementations --- spec/unit/rendezvous/ecdh.spec.ts | 10 +++++----- .../rendezvous/simpleHttpTransport.spec.ts | 18 +++++++++--------- src/rendezvous/channels/ecdhV1.ts | 2 +- src/rendezvous/index.ts | 10 +++++----- src/rendezvous/rendezvous.ts | 4 ++-- .../transports/simpleHttpTransport.ts | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 223a23577c5..7cfbc3c96f0 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -24,7 +24,7 @@ import { RendezvousTransport, RendezvousTransportDetails, } from "../../../src/rendezvous"; -import { ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; +import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; import { setCrypto, sleep } from '../../../src/utils'; @@ -84,9 +84,9 @@ describe("ECDHv1", function() { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); @@ -106,9 +106,9 @@ describe("ECDHv1", function() { bobTransport.otherParty = aliceTransport; // alice is signing in initiates and generates a code - const alice = new ECDHv1RendezvousChannel(aliceTransport); + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); const bobChecksum = await bob.connect(); const aliceChecksum = await alice.connect(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 2c5f6ee7dab..26ed94ef2e9 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; -import { SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; +import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; describe("SimpleHttpRendezvousTransport", function() { let httpBackend: MockHttpBackend; @@ -32,7 +32,7 @@ describe("SimpleHttpRendezvousTransport", function() { locationResponse: string, expectedFinalLocation: string, ) { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); { // initial POST const prom = simpleHttpTransport.send("application/json", {}); httpBackend.when("POST", fallbackRzServer).response = { @@ -65,12 +65,12 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ fetchFn }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fetchFn }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -101,7 +101,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST to follow 307 to other server", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -130,7 +130,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and GET", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -186,7 +186,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and PUTs", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -243,7 +243,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init with URI", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetchFn, }); @@ -265,7 +265,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("init from HS", async function() { - const simpleHttpTransport = new SimpleHttpRendezvousTransport({ + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ rendezvousUri: "https://server/rz/123", fetchFn, }); diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index f116717d661..1b852ed9f86 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -85,7 +85,7 @@ async function importKey(key: Uint8Array): Promise { * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) * X25519/ECDH key agreement based secure rendezvous channel. */ -export class ECDHv1RendezvousChannel implements RendezvousChannel { +export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey | Uint8Array; diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 646b9dc3975..437909eb3d5 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -18,9 +18,9 @@ import { RendezvousFailureListener, RendezvousFailureReason } from './cancellati import { RendezvousChannel } from './channel'; import { RendezvousCode, RendezvousIntent } from './code'; import { RendezvousError } from './error'; -import { SimpleHttpRendezvousTransport, SimpleHttpRendezvousTransportDetails } from './transports'; +import { MSC3886SimpleHttpRendezvousTransport, MSC3886SimpleHttpRendezvousTransportDetails } from './transports'; import { decodeBase64 } from '../crypto/olmlib'; -import { ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; +import { MSC3903ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; import { logger } from '../logger'; export * from './code'; @@ -53,7 +53,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedTransport); } - const transportDetails = rendezvous.transport as SimpleHttpRendezvousTransportDetails; + const transportDetails = rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails; if (typeof transportDetails.uri !== 'string') { throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); @@ -63,7 +63,7 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); + const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); @@ -75,7 +75,7 @@ export async function buildChannelFromCode( logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); return { - channel: new ECDHv1RendezvousChannel(transport, theirPublicKey), + channel: new MSC3903ECDHv1RendezvousChannel(transport, theirPublicKey), intent, }; } diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 65b16192835..1bbf51bccc2 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -26,13 +26,13 @@ import { sleep } from "../utils"; import { RendezvousFailureReason } from "./cancellationReason"; import { RendezvousIntent } from "./code"; -export enum PayloadType { +enum PayloadType { Start = 'm.login.start', Finish = 'm.login.finish', Progress = 'm.login.progress', } -export class Rendezvous { +export class MSC3906Rendezvous { private cli?: MatrixClient; private newDeviceId?: string; private newDeviceKey?: string; diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 5816320c46a..87a772fd7cb 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -21,7 +21,7 @@ import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; import { ClientPrefix } from '../../http-api'; -export interface SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { +export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { type: 'http.v1'; uri: string; } @@ -30,7 +30,7 @@ export interface SimpleHttpRendezvousTransportDetails extends RendezvousTranspor * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) * simple HTTP rendezvous protocol. */ -export class SimpleHttpRendezvousTransport implements RendezvousTransport { +export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { ready = false; cancelled = false; private uri?: string; @@ -66,7 +66,7 @@ export class SimpleHttpRendezvousTransport implements RendezvousTransport { this.ready = !!this.uri; } - async details(): Promise { + async details(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } From 0853f31ac8c836a0c1e6cbb5c1c180bf682b8b7e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 14:24:55 +0100 Subject: [PATCH 28/74] Linting --- src/rendezvous/index.ts | 6 +- src/rendezvous/rendezvous.ts | 77 ++++++++++++++----- .../transports/simpleHttpTransport.ts | 2 +- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 437909eb3d5..a7b5c6fee31 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -63,7 +63,11 @@ export async function buildChannelFromCode( throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); } - const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure, rendezvousUri: transportDetails.uri, fetchFn }); + const transport = new MSC3886SimpleHttpRendezvousTransport({ + onFailure, + rendezvousUri: transportDetails.uri, + fetchFn, + }); if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 1bbf51bccc2..3621729853b 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -222,26 +222,31 @@ export class MSC3906Rendezvous { verifying_device_key: verifyingDeviceKey, } = await this.channel.receive(); + if (!verifyingDeviceId || !verifyingDeviceKey) { + logger.warn("No verifying_device_id or verifying_device_key received"); + return; + } + const userId = client.getUserId()!; const verifyingDeviceFromServer = - client.crypto.deviceList.getStoredDevice(userId, verifyingDeviceId); - - if (verifyingDeviceFromServer?.getFingerprint() === verifyingDeviceKey) { - // set other device as verified - logger.info(`Setting device ${verifyingDeviceId} as verified`); - await client.setDeviceVerified(userId, verifyingDeviceId, true); + client.crypto?.deviceList.getStoredDevice(userId, verifyingDeviceId); - if (masterKey) { - // set master key as trusted - await client.setDeviceVerified(userId, masterKey, true); - } + if (verifyingDeviceFromServer?.getFingerprint() !== verifyingDeviceKey) { + logger.warn(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + return; + } + // set other device as verified + logger.info(`Setting device ${verifyingDeviceId} as verified`); + await client.setDeviceVerified(userId, verifyingDeviceId, true); - // request secrets from the verifying device - logger.info(`Requesting secrets from ${verifyingDeviceId}`); - await requestKeysDuringVerification(client, userId, verifyingDeviceId); - } else { - logger.info(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); + if (masterKey) { + // set master key as trusted + await client.setDeviceVerified(userId, masterKey, true); } + + // request secrets from the verifying device + logger.info(`Requesting secrets from ${verifyingDeviceId}`); + await requestKeysDuringVerification(client, userId, verifyingDeviceId); } async declineLoginOnExistingDevice() { @@ -250,6 +255,10 @@ export class MSC3906Rendezvous { } async confirmLoginOnExistingDevice(): Promise { + if (!this.cli) { + throw new Error('No client set'); + } + const client = this.cli; logger.info("Requesting login token"); @@ -284,6 +293,18 @@ export class MSC3906Rendezvous { } private async checkAndCrossSignDevice(deviceInfo: DeviceInfo) { + if (!this.cli) { + throw new Error('No client set'); + } + + if (!this.cli.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( @@ -291,10 +312,15 @@ export class MSC3906Rendezvous { ); } + const userId = this.cli.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.cli.crypto.setDeviceVerification( - this.cli.getUserId(), + userId, this.newDeviceId, true, false, true, ); @@ -321,11 +347,24 @@ export class MSC3906Rendezvous { logger.info("No new device key to sign"); return undefined; } + const client = this.cli; + + if (!client) { + throw new Error('No client set'); + } - const cli = this.cli; + if (!client.crypto) { + throw new Error('Crypto not available on client'); + } + + const userId = client.getUserId(); + + if (!userId) { + throw new Error('No user ID set'); + } { - const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.checkAndCrossSignDevice(deviceInfo); @@ -338,7 +377,7 @@ export class MSC3906Rendezvous { logger.info("Going to wait for new device to be online"); { - const deviceInfo = cli.crypto.getStoredDevice(cli.getUserId(), this.newDeviceId); + const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.checkAndCrossSignDevice(deviceInfo); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 87a772fd7cb..12433fa812a 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -40,7 +40,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport private client?: MatrixClient; private hsUrl?: string; private fallbackRzServer?: string; - private fetchFn: typeof global.fetch; + private fetchFn?: typeof global.fetch; constructor({ onFailure, From aaf5ca26de0d64daa544e8d61fdb1591e7911196 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 22:58:49 +0100 Subject: [PATCH 29/74] Reduce log noise --- src/rendezvous/channels/ecdhV1.ts | 17 ++++++++--------- src/rendezvous/rendezvous.ts | 8 ++------ .../transports/simpleHttpTransport.ts | 12 ++++++------ 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 1b852ed9f86..c8b163f6275 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -16,7 +16,6 @@ limitations under the License. import { SAS } from '@matrix-org/olm'; -import { logger } from '../../logger'; import { RendezvousError } from '../error'; import { RendezvousCode, @@ -127,7 +126,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { if (isInitiator) { // wait for the other side to send us their public key - logger.info('Waiting for other device to send their public key'); + // logger.info('Waiting for other device to send their public key'); const res = await this.receive(); if (!res) { throw new Error('No response from other device'); @@ -162,10 +161,10 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.aesKey = await importKey(aesKeyBytes); - logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); - logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey!)}`); - logger.debug(`AES info: ${aesInfo}`); - logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); + // logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); + // logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey!)}`); + // logger.debug(`AES info: ${aesInfo}`); + // logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); return generateDecimalSas(Array.from(rawChecksum)); @@ -222,7 +221,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const stringifiedData = JSON.stringify(data); if (this.aesKey) { - logger.info(`Encrypting: ${stringifiedData}`); + // logger.info(`Encrypting: ${stringifiedData}`); await this.transport.send('application/json', await this.encrypt(stringifiedData)); } else { await this.transport.send('application/json', stringifiedData); @@ -271,7 +270,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const data = await this.transport.receive(); - logger.info(`Received data: ${JSON.stringify(data)}`); + // logger.info(`Received data: ${JSON.stringify(data)}`); if (!data) { return data; } @@ -281,7 +280,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Shared secret not set up'); } const decrypted = await this.decrypt(data); - logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); + // logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); return JSON.parse(decrypted); } else if (this.aesKey) { throw new Error('Data received but no ciphertext'); diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 3621729853b..b39624c66b4 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -62,8 +62,6 @@ export class MSC3906Rendezvous { private async areIntentsIncompatible(theirIntent: RendezvousIntent): Promise { const incompatible = theirIntent === this.ourIntent; - logger.info(`ourIntent: ${this.ourIntent}, theirIntent: ${theirIntent}, incompatible: ${incompatible}`); - if (incompatible) { await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); await this.channel.cancel( @@ -250,7 +248,7 @@ export class MSC3906Rendezvous { } async declineLoginOnExistingDevice() { - logger.info('User declined linking'); + logger.info('User declined sign in'); await this.send({ type: PayloadType.Finish, outcome: 'declined' }); } @@ -371,10 +369,8 @@ export class MSC3906Rendezvous { } } - logger.info("New device is not online"); - await sleep(timeout); - logger.info("Going to wait for new device to be online"); + await sleep(timeout); { const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 12433fa812a..8dc01c14080 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -115,7 +115,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport throw new Error('Invalid rendezvous URI'); } - logger.debug(`Sending data: ${data} to ${uri}`); + // logger.debug(`Sending data: ${data} to ${uri}`); const headers: Record = { 'content-type': contentType }; if (this.etag) { @@ -131,7 +131,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } this.etag = res.headers.get("etag") ?? undefined; - logger.debug(`Posted data to ${uri} new etag ${this.etag}`); + // logger.debug(`Posted data to ${uri} new etag ${this.etag}`); if (method === 'POST') { const location = res.headers.get('location'); @@ -160,14 +160,14 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport if (this.cancelled) { return; } - logger.debug(`Polling: ${this.uri} after etag ${this.etag}`); + // logger.debug(`Polling: ${this.uri} after etag ${this.etag}`); const headers: Record = {}; if (this.etag) { headers['if-none-match'] = this.etag; } const poll = await this.fetch(this.uri, { method: "GET", headers }); - logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); + // logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); if (poll.status === 404) { return this.cancel(RendezvousFailureReason.Unknown); } @@ -179,7 +179,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } else if (poll.status === 200) { this.etag = poll.headers.get("etag") ?? undefined; const data = await poll.json(); - logger.debug(`Received data: ${JSON.stringify(data)} from ${this.uri} with etag ${this.etag}`); + // logger.debug(`Received data: ${JSON.stringify(data)} from ${this.uri} with etag ${this.etag}`); return data; } await sleep(1000); @@ -198,7 +198,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport if (this.uri && reason === RendezvousFailureReason.UserDeclined) { try { - logger.debug(`Deleting channel: ${this.uri}`); + // logger.debug(`Deleting channel: ${this.uri}`); await this.fetch(this.uri, { method: "DELETE" }); } catch (e) { logger.warn(e); From 4d37fc231ac3a4b4c4f08a28529e8c694e13976e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 23:03:49 +0100 Subject: [PATCH 30/74] Tidy up interface a bit --- src/rendezvous/rendezvous.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index b39624c66b4..126f3081576 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -87,7 +87,7 @@ export class MSC3906Rendezvous { const checksum = await this.channel.connect(); - logger.info(`Connected to secure channel with checksum: ${checksum}`); + logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); if (didScan) { if (await this.areIntentsIncompatible(theirIntent)) { @@ -156,7 +156,7 @@ export class MSC3906Rendezvous { return checksum; } - async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { + private async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { await this.channel.send({ type, ...payload }); } @@ -290,7 +290,7 @@ export class MSC3906Rendezvous { return deviceId; } - private async checkAndCrossSignDevice(deviceInfo: DeviceInfo) { + private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo) { if (!this.cli) { throw new Error('No client set'); } @@ -336,7 +336,7 @@ export class MSC3906Rendezvous { return info; } - async crossSign(timeout = 10 * 1000): Promise { + async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise { if (!this.newDeviceId) { throw new Error('No new device to sign'); } @@ -365,7 +365,7 @@ export class MSC3906Rendezvous { const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { - return await this.checkAndCrossSignDevice(deviceInfo); + return await this.verifyAndCrossSignDevice(deviceInfo); } } @@ -376,7 +376,7 @@ export class MSC3906Rendezvous { const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { - return await this.checkAndCrossSignDevice(deviceInfo); + return await this.verifyAndCrossSignDevice(deviceInfo); } } From d64cd99bc6e179226670fac9b8f4c90d42ff253d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 23:23:03 +0100 Subject: [PATCH 31/74] Additional test for transport layer --- .../rendezvous/simpleHttpTransport.spec.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 26ed94ef2e9..542ab78ee1f 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -15,6 +15,7 @@ limitations under the License. */ import MockHttpBackend from "matrix-mock-request"; +import { RendezvousFailureReason } from "../../../src/rendezvous"; import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; @@ -285,4 +286,40 @@ describe("SimpleHttpRendezvousTransport", function() { expect(await prom).toEqual({ foo: "baa" }); } }); + + it("POST and DELETE", async function() { + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + { // Create + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { + expect(headers["content-type"]).toEqual("application/json"); + expect(data).toEqual({ foo: "baa" }); + }).response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + }, + }, + }; + await httpBackend.flush(''); + expect(await prom).toStrictEqual(undefined); + } + { // Cancel + const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined); + httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = { + body: null, + response: { + statusCode: 204, + headers: {}, + }, + }; + await httpBackend.flush(''); + await prom; + } + }); }); From 22e4fe87437df0b11eef807a20eade63080172b7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 23:28:47 +0100 Subject: [PATCH 32/74] Linting --- spec/unit/rendezvous/simpleHttpTransport.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 542ab78ee1f..3bcd2a505e0 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -15,8 +15,8 @@ limitations under the License. */ import MockHttpBackend from "matrix-mock-request"; -import { RendezvousFailureReason } from "../../../src/rendezvous"; +import { RendezvousFailureReason } from "../../../src/rendezvous"; import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; describe("SimpleHttpRendezvousTransport", function() { From a81b70e706bbc0ae948c775a56467deedc724cf3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 13 Oct 2022 23:29:01 +0100 Subject: [PATCH 33/74] Refactor dummy transport to be re-usable --- spec/unit/rendezvous/DummyTransport.ts | 65 ++++++++++++++++++++++++++ spec/unit/rendezvous/ecdh.spec.ts | 62 +++--------------------- 2 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 spec/unit/rendezvous/DummyTransport.ts diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts new file mode 100644 index 00000000000..d5579232444 --- /dev/null +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -0,0 +1,65 @@ +/* +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, + RendezvousTransport, + RendezvousTransportDetails, +} from "../../../src/rendezvous"; +import { sleep } from '../../../src/utils'; + +export class DummyTransport implements RendezvousTransport { + otherParty?: DummyTransport; + etag?: string; + data = null; + + ready = false; + + constructor(private mockDetails: RendezvousTransportDetails) {} + onCancelled?: RendezvousFailureListener; + + details(): Promise { + return Promise.resolve(this.mockDetails); + } + + async send(contentType: string, data: any): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag === this.etag) { + this.data = data; + this.etag = Math.random().toString(); + return; + } + await sleep(100); + } + } + + async receive(): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!this.etag || this.otherParty?.etag !== this.etag) { + this.etag = this.otherParty?.etag; + return this.otherParty?.data ? JSON.parse(this.otherParty.data) : undefined; + } + await sleep(100); + } + } + + cancel(reason: RendezvousFailureReason): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 7cfbc3c96f0..3861fb3ae0d 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -17,59 +17,11 @@ limitations under the License. import crypto from 'crypto'; import '../../olm-loader'; -import { - RendezvousFailureListener, - RendezvousFailureReason, - RendezvousIntent, - RendezvousTransport, - RendezvousTransportDetails, -} from "../../../src/rendezvous"; +import { RendezvousIntent } from "../../../src/rendezvous"; import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; -import { setCrypto, sleep } from '../../../src/utils'; - -class DummyTransport implements RendezvousTransport { - otherParty?: DummyTransport; - etag?: string; - data = null; - - ready = false; - - onCancelled?: RendezvousFailureListener; - - details(): Promise { - return Promise.resolve({ - type: 'dummy', - }); - } - - async send(contentType: string, data: any): Promise { - // eslint-disable-next-line no-constant-condition - while (true) { - if (!this.etag || this.otherParty?.etag === this.etag) { - this.data = data; - this.etag = Math.random().toString(); - return; - } - await sleep(100); - } - } - - async receive(): Promise { - // eslint-disable-next-line no-constant-condition - while (true) { - if (!this.etag || this.otherParty?.etag !== this.etag) { - this.etag = this.otherParty?.etag; - return this.otherParty?.data ? JSON.parse(this.otherParty.data) : undefined; - } - await sleep(100); - } - } - - cancel(reason: RendezvousFailureReason): Promise { - throw new Error("Method not implemented."); - } -} +import { setCrypto } from '../../../src/utils'; +import { DummyTransport } from './DummyTransport'; describe("ECDHv1", function() { beforeAll(async function() { @@ -78,8 +30,8 @@ describe("ECDHv1", function() { }); it("initiator wants to sign in", async function() { - const aliceTransport = new DummyTransport(); - const bobTransport = new DummyTransport(); + const aliceTransport = new DummyTransport({ type: 'dummy' }); + const bobTransport = new DummyTransport({ type: 'dummy' }); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -100,8 +52,8 @@ describe("ECDHv1", function() { }); it("initiator wants to reciprocate", async function() { - const aliceTransport = new DummyTransport(); - const bobTransport = new DummyTransport(); + const aliceTransport = new DummyTransport({ type: 'dummy' }); + const bobTransport = new DummyTransport({ type: 'dummy' }); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; From 33b31d053ce16f11e654023e9c4c192dc356316f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 02:16:26 +0100 Subject: [PATCH 34/74] Remove redundant condition --- src/rendezvous/channels/ecdhV1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index c8b163f6275..88331527fba 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -133,7 +133,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const { key, algorithm } = res; - if (algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1 || (isInitiator && !key)) { + if (algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1 || !key) { throw new RendezvousError( 'Unsupported algorithm: ' + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, From 9d3a6da954041cd0f0b7a71bea11196e800ee031 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 03:47:03 +0100 Subject: [PATCH 35/74] Handle more error cases --- src/rendezvous/channels/ecdhV1.ts | 2 +- src/rendezvous/index.ts | 2 +- src/rendezvous/rendezvous.ts | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 88331527fba..e2394e66736 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -88,11 +88,11 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey | Uint8Array; - public onFailure?: (reason: RendezvousFailureReason) => void; constructor( public transport: RendezvousTransport, private theirPublicKey?: Uint8Array, + public onFailure?: (reason: RendezvousFailureReason) => void, ) { this.olmSAS = new global.Olm.SAS(); this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index a7b5c6fee31..9f0f2967225 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -79,7 +79,7 @@ export async function buildChannelFromCode( logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); return { - channel: new MSC3903ECDHv1RendezvousChannel(transport, theirPublicKey), + channel: new MSC3903ECDHv1RendezvousChannel(transport, theirPublicKey, onFailure), intent, }; } diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 126f3081576..ed0221bbef9 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -38,9 +38,12 @@ export class MSC3906Rendezvous { private newDeviceKey?: string; private ourIntent: RendezvousIntent; public code?: string; - public onFailure?: (reason: RendezvousFailureReason) => void; - constructor(public channel: RendezvousChannel, cli?: MatrixClient) { + constructor( + public channel: RendezvousChannel, + cli?: MatrixClient, + public onFailure?: (reason: RendezvousFailureReason) => void, + ) { this.cli = cli; this.ourIntent = this.isNewDevice ? RendezvousIntent.LOGIN_ON_NEW_DEVICE : @@ -176,6 +179,9 @@ export class MSC3906Rendezvous { case 'unsupported': await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); break; + case 'declined': + await this.cancel(RendezvousFailureReason.UserDeclined); + break; default: await this.cancel(RendezvousFailureReason.Unknown); } @@ -388,6 +394,7 @@ export class MSC3906Rendezvous { } async cancel(reason: RendezvousFailureReason) { + this.onFailure?.(reason); await this.channel.cancel(reason); } From 4bf908078561c7f547f7f2d28809f92d7bd9a75f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 14 Oct 2022 03:47:16 +0100 Subject: [PATCH 36/74] Initial tests for MSC3906 --- spec/unit/rendezvous/DummyTransport.ts | 52 ++++++-- spec/unit/rendezvous/ecdh.spec.ts | 8 +- spec/unit/rendezvous/rendezvous.spec.ts | 150 +++++++++++++++++++++++- 3 files changed, 193 insertions(+), 17 deletions(-) diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts index d5579232444..a2e80d00fb9 100644 --- a/spec/unit/rendezvous/DummyTransport.ts +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { logger } from "../../../src/logger"; import { RendezvousFailureListener, RendezvousFailureReason, @@ -22,14 +23,16 @@ import { } from "../../../src/rendezvous"; import { sleep } from '../../../src/utils'; -export class DummyTransport implements RendezvousTransport { - otherParty?: DummyTransport; +export class DummyTransport implements RendezvousTransport { + otherParty?: DummyTransport; etag?: string; + lastEtagReceived?: string; data = null; ready = false; + cancelled = false; - constructor(private mockDetails: RendezvousTransportDetails) {} + constructor(private name: string, private mockDetails: T) {} onCancelled?: RendezvousFailureListener; details(): Promise { @@ -37,29 +40,54 @@ export class DummyTransport implements RendezvousTransport { } async send(contentType: string, data: any): Promise { + logger.info( + `[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${ + data} of type ${contentType} where etag matches ${this.etag}`, + ); // eslint-disable-next-line no-constant-condition - while (true) { - if (!this.etag || this.otherParty?.etag === this.etag) { + while (!this.cancelled) { + if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) { this.data = data; this.etag = Math.random().toString(); + this.lastEtagReceived = this.etag; + this.otherParty!.etag = this.etag; + this.otherParty!.data = data; + logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`); return; } - await sleep(100); + logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`); + await sleep(250); } } async receive(): Promise { + logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`); // eslint-disable-next-line no-constant-condition - while (true) { - if (!this.etag || this.otherParty?.etag !== this.etag) { - this.etag = this.otherParty?.etag; - return this.otherParty?.data ? JSON.parse(this.otherParty.data) : undefined; + while (!this.cancelled) { + if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) { + this.lastEtagReceived = this.etag; + const data = this.data ? JSON.parse(this.data) : undefined; + logger.info( + `[${this.otherParty?.name}] => [${this.name}] Received data: ` + + `${JSON.stringify(data)} with etag ${this.etag}`, + ); + return data; } - await sleep(100); + logger.info(`[${this.name}] Sleeping to retry receive after etag ${ + this.lastEtagReceived} as remote is ${this.etag}`); + await sleep(250); } + + return undefined; } cancel(reason: RendezvousFailureReason): Promise { - throw new Error("Method not implemented."); + this.cancelled = true; + this.onCancelled?.(reason); + return Promise.resolve(); + } + + cleanup() { + this.cancelled = true; } } diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 3861fb3ae0d..b0262fc6f64 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -30,8 +30,8 @@ describe("ECDHv1", function() { }); it("initiator wants to sign in", async function() { - const aliceTransport = new DummyTransport({ type: 'dummy' }); - const bobTransport = new DummyTransport({ type: 'dummy' }); + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -52,8 +52,8 @@ describe("ECDHv1", function() { }); it("initiator wants to reciprocate", async function() { - const aliceTransport = new DummyTransport({ type: 'dummy' }); - const bobTransport = new DummyTransport({ type: 'dummy' }); + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 6c1ecbd7c28..0c32da82f0f 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -17,7 +17,24 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; -import { buildChannelFromCode } from "../../../src/rendezvous"; +import { buildChannelFromCode, MSC3906Rendezvous, RendezvousFailureReason } from "../../../src/rendezvous"; +import { DummyTransport } from "./DummyTransport"; +import { MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { MatrixClient } from "../../../src"; + +function makeMockClient(opts: { userId: string, deviceId: string, msc3882Enabled: boolean}): MatrixClient { + return { + doesServerSupportUnstableFeature(feature: string) { + return Promise.resolve(opts.msc3882Enabled && feature === "org.matrix.msc3882"); + }, + getUserId() { return opts.userId; }, + getDeviceId() { return opts.deviceId; }, + requestLoginToken() { + return Promise.resolve({ login_token: "token" }); + }, + baseUrl: "https://example.com", + } as unknown as MatrixClient; +} describe("Rendezvous", function() { beforeAll(async function() { @@ -26,10 +43,16 @@ describe("Rendezvous", function() { let httpBackend: MockHttpBackend; let fetchFn: typeof global.fetchFn; + let transports: DummyTransport[]; beforeEach(function() { httpBackend = new MockHttpBackend(); fetchFn = httpBackend.fetchFn as typeof global.fetch; + transports = []; + }); + + afterEach(function() { + transports.forEach(x => x.cleanup()); }); describe("buildChannelFromCode", function() { @@ -126,4 +149,129 @@ describe("Rendezvous", function() { expect(await prom).toStrictEqual({}); }); }); + + describe("end-to-end", function() { + it("generate on new device and scan on existing - decline", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + try { + // alice is signing in initiates and generates a code + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceRz = new MSC3906Rendezvous(aliceEcdh); + const aliceOnFailure = jest.fn(); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is already signed in and scans the code + const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: true }); + const { + channel: bobEcdh, + intent: aliceIntentAsSeenByBob, + } = await buildChannelFromCode(aliceRz.code!, () => {}, fetchFn); + // we override the channel to set to dummy transport: + (bobEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = bobTransport; + const bobRz = new MSC3906Rendezvous(bobEcdh, bob); + const bobStartProm = bobRz.startAfterScanningCode(aliceIntentAsSeenByBob); + + // check that the two sides are talking to each other with same checksum + const aliceChecksum = await aliceStartProm; + const bobChecksum = await bobStartProm; + expect(aliceChecksum).toEqual(bobChecksum); + + const aliceCompleteProm = aliceRz.completeLoginOnNewDevice(); + await bobRz.declineLoginOnExistingDevice(); + + await aliceCompleteProm; + expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UserDeclined); + } finally { + aliceTransport.cleanup(); + bobTransport.cleanup(); + } + }); + + it("generate on existing device and scan on new device - decline", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + try { + // bob is already signed initiates and generates a code + const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: true }); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel(bobTransport); + const bobRz = new MSC3906Rendezvous(bobEcdh, bob); + await bobRz.generateCode(); + const bobStartProm = bobRz.startAfterShowingCode(); + + // alice wants to sign in and scans the code + const aliceOnFailure = jest.fn(); + const { + channel: aliceEcdh, + intent: bobsIntentAsSeenByAlice, + } = await buildChannelFromCode(bobRz.code!, aliceOnFailure, fetchFn); + // we override the channel to set to dummy transport: + (aliceEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = aliceTransport; + const aliceRz = new MSC3906Rendezvous(aliceEcdh, undefined, aliceOnFailure); + const aliceStartProm = aliceRz.startAfterScanningCode(bobsIntentAsSeenByAlice); + + // check that the two sides are talking to each other with same checksum + const bobChecksum = await bobStartProm; + const aliceChecksum = await aliceStartProm; + expect(aliceChecksum).toEqual(bobChecksum); + + const aliceCompleteProm = aliceRz.completeLoginOnNewDevice(); + await bobRz.declineLoginOnExistingDevice(); + + await aliceCompleteProm; + expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UserDeclined); + } finally { + aliceTransport.cleanup(); + bobTransport.cleanup(); + } + }); + + it("no protocol available", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + try { + // alice is signing in initiates and generates a code + const aliceOnFailure = jest.fn(); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, undefined, aliceOnFailure); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is already signed in and scans the code + const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: false }); + const { + channel: bobEcdh, + intent: aliceIntentAsSeenByBob, + } = await buildChannelFromCode(aliceRz.code!, () => {}, fetchFn); + // we override the channel to set to dummy transport: + (bobEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = bobTransport; + const bobRz = new MSC3906Rendezvous(bobEcdh, bob); + const bobStartProm = bobRz.startAfterScanningCode(aliceIntentAsSeenByBob); + + // check that the two sides are talking to each other with same checksum + const aliceChecksum = await aliceStartProm; + const bobChecksum = await bobStartProm; + expect(bobChecksum).toBeUndefined(); + expect(aliceChecksum).toBeUndefined(); + + expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm); + // await aliceRz.completeLoginOnNewDevice(); + } finally { + aliceTransport.cleanup(); + bobTransport.cleanup(); + } + }); + }); }); From 0fc06084c7a56e6ed4fcdca32b3707612e18d211 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 18:25:32 +0100 Subject: [PATCH 37/74] Reduce scope of PR to only cover generating a code on existing device --- spec/unit/rendezvous/ecdh.spec.ts | 34 ++- spec/unit/rendezvous/rendezvous.spec.ts | 264 ++++-------------- .../rendezvous/simpleHttpTransport.spec.ts | 114 ++++---- src/rendezvous/channels/ecdhV1.ts | 1 + src/rendezvous/index.ts | 65 +---- src/rendezvous/rendezvous.ts | 258 ++++------------- .../transports/simpleHttpTransport.ts | 30 +- 7 files changed, 200 insertions(+), 566 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index b0262fc6f64..fce579d02a6 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -17,13 +17,13 @@ limitations under the License. import crypto from 'crypto'; import '../../olm-loader'; -import { RendezvousIntent } from "../../../src/rendezvous"; +import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; -import { setCrypto } from '../../../src/utils'; import { DummyTransport } from './DummyTransport'; +import { setCrypto } from '../../../src/utils'; -describe("ECDHv1", function() { +describe('ECDHv1', function() { beforeAll(async function() { setCrypto(crypto); await global.Olm.init(); @@ -49,6 +49,9 @@ describe("ECDHv1", function() { await alice.send(message); const bobReceive = await bob.receive(); expect(bobReceive).toEqual(message); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); }); it("initiator wants to reciprocate", async function() { @@ -71,5 +74,30 @@ describe("ECDHv1", function() { await bob.send(message); const aliceReceive = await alice.receive(); expect(aliceReceive).toEqual(message); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("double connect", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + expect(alice.connect()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); }); }); diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 0c32da82f0f..461724f4c85 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -17,15 +17,27 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; -import { buildChannelFromCode, MSC3906Rendezvous, RendezvousFailureReason } from "../../../src/rendezvous"; -import { DummyTransport } from "./DummyTransport"; +import { MSC3906Rendezvous, RendezvousCode, RendezvousIntent } from "../../../src/rendezvous"; import { MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; import { MatrixClient } from "../../../src"; - -function makeMockClient(opts: { userId: string, deviceId: string, msc3882Enabled: boolean}): MatrixClient { +import { + MSC3886SimpleHttpRendezvousTransport, + MSC3886SimpleHttpRendezvousTransportDetails, +} from "../../../src/rendezvous/transports"; + +function makeMockClient(opts: { + userId: string; + deviceId: string; + msc3882Enabled: boolean; + msc3886Enabled: boolean; +}): MatrixClient { return { doesServerSupportUnstableFeature(feature: string) { - return Promise.resolve(opts.msc3882Enabled && feature === "org.matrix.msc3882"); + switch (feature) { + case "org.matrix.msc3882": return opts.msc3882Enabled; + case "org.matrix.msc3886": return opts.msc3886Enabled; + default: return false; + } }, getUserId() { return opts.userId; }, getDeviceId() { return opts.deviceId; }, @@ -43,235 +55,53 @@ describe("Rendezvous", function() { let httpBackend: MockHttpBackend; let fetchFn: typeof global.fetchFn; - let transports: DummyTransport[]; beforeEach(function() { httpBackend = new MockHttpBackend(); fetchFn = httpBackend.fetchFn as typeof global.fetch; - transports = []; - }); - - afterEach(function() { - transports.forEach(x => x.cleanup()); }); - describe("buildChannelFromCode", function() { - it("non-JSON", function() { - expect(buildChannelFromCode("xyz", () => {}, fetchFn)).rejects.toThrow("Invalid code"); - }); - - it("invalid JSON", function() { - expect(buildChannelFromCode(JSON.stringify({}), () => {}, fetchFn)) - .rejects.toThrow("Unsupported transport"); - }); - - it("invalid transport type", function() { - expect(buildChannelFromCode(JSON.stringify({ - rendezvous: { transport: { type: "foo" } }, - }), () => {}, fetchFn)).rejects.toThrow("Unsupported transport"); - }); - - it("missing URI", function() { - expect(buildChannelFromCode(JSON.stringify({ - rendezvous: { transport: { type: "http.v1" } }, - }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); - }); - - it("invalid URI field", function() { - expect(buildChannelFromCode(JSON.stringify({ - rendezvous: { transport: { type: "http.v1", uri: false } }, - }), () => {}, fetchFn)).rejects.toThrow("Invalid code"); - }); - - it("missing intent", function() { - expect(buildChannelFromCode(JSON.stringify({ - rendezvous: { transport: { type: "http.v1", uri: "something" } }, - }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); - }); - - it("invalid intent", function() { - expect(buildChannelFromCode(JSON.stringify({ - intent: 'asd', - rendezvous: { - algorithm: "m.rendezvous.v1.curve25519-aes-sha256", - key: "", - transport: { type: "http.v1", uri: "something" }, - }, - }), () => {}, fetchFn)).rejects.toThrow("Invalid intent"); - }); - - it("login.reciprocate", async function() { - const x = await buildChannelFromCode(JSON.stringify({ - intent: 'login.reciprocate', - rendezvous: { - algorithm: "m.rendezvous.v1.curve25519-aes-sha256", - key: "", - transport: { type: "http.v1", uri: "something" }, - }, - }), () => {}, fetchFn); - expect(x.intent).toBe("login.reciprocate"); - }); - - it("login.start", async function() { - const x = await buildChannelFromCode(JSON.stringify({ - intent: 'login.start', - rendezvous: { - algorithm: "m.rendezvous.v1.curve25519-aes-sha256", - key: "", - transport: { type: "http.v1", uri: "something" }, - }, - }), () => {}, fetchFn); - expect(x.intent).toBe("login.start"); - }); - - it("parse and get", async function() { - const x = await buildChannelFromCode(JSON.stringify({ - intent: 'login.start', - rendezvous: { - algorithm: "m.rendezvous.v1.curve25519-aes-sha256", - key: "", - transport: { type: "http.v1", uri: "https://rz.server/123456" }, - }, - }), () => {}, fetchFn); - expect(x.intent).toBe("login.start"); - - const prom = x.channel.receive(); - httpBackend.when("GET", "https://rz.server/123456").response = { - body: {}, + describe("end-to-end", function() { + it("generate", async function() { + const alice = makeMockClient({ + userId: "@alice:example.com", + deviceId: "DEVICEID", + msc3886Enabled: false, + msc3882Enabled: true, + }); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, response: { - statusCode: 200, + statusCode: 201, headers: { - "content-type": "application/json", + location: "https://fallbackserver/rz/123", }, }, }; - await httpBackend.flush(''); - expect(await prom).toStrictEqual({}); - }); - }); - - describe("end-to-end", function() { - it("generate on new device and scan on existing - decline", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - try { - // alice is signing in initiates and generates a code - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceRz = new MSC3906Rendezvous(aliceEcdh); - const aliceOnFailure = jest.fn(); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const aliceStartProm = aliceRz.startAfterShowingCode(); - - // bob is already signed in and scans the code - const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: true }); - const { - channel: bobEcdh, - intent: aliceIntentAsSeenByBob, - } = await buildChannelFromCode(aliceRz.code!, () => {}, fetchFn); - // we override the channel to set to dummy transport: - (bobEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = bobTransport; - const bobRz = new MSC3906Rendezvous(bobEcdh, bob); - const bobStartProm = bobRz.startAfterScanningCode(aliceIntentAsSeenByBob); - - // check that the two sides are talking to each other with same checksum - const aliceChecksum = await aliceStartProm; - const bobChecksum = await bobStartProm; - expect(aliceChecksum).toEqual(bobChecksum); + const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({ + client: alice, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - const aliceCompleteProm = aliceRz.completeLoginOnNewDevice(); - await bobRz.declineLoginOnExistingDevice(); + expect(aliceRz.code).toBeUndefined(); - await aliceCompleteProm; - expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UserDeclined); - } finally { - aliceTransport.cleanup(); - bobTransport.cleanup(); - } - }); - - it("generate on existing device and scan on new device - decline", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - try { - // bob is already signed initiates and generates a code - const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: true }); - const bobEcdh = new MSC3903ECDHv1RendezvousChannel(bobTransport); - const bobRz = new MSC3906Rendezvous(bobEcdh, bob); - await bobRz.generateCode(); - const bobStartProm = bobRz.startAfterShowingCode(); - - // alice wants to sign in and scans the code - const aliceOnFailure = jest.fn(); - const { - channel: aliceEcdh, - intent: bobsIntentAsSeenByAlice, - } = await buildChannelFromCode(bobRz.code!, aliceOnFailure, fetchFn); - // we override the channel to set to dummy transport: - (aliceEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = aliceTransport; - const aliceRz = new MSC3906Rendezvous(aliceEcdh, undefined, aliceOnFailure); - const aliceStartProm = aliceRz.startAfterScanningCode(bobsIntentAsSeenByAlice); - - // check that the two sides are talking to each other with same checksum - const bobChecksum = await bobStartProm; - const aliceChecksum = await aliceStartProm; - expect(aliceChecksum).toEqual(bobChecksum); - - const aliceCompleteProm = aliceRz.completeLoginOnNewDevice(); - await bobRz.declineLoginOnExistingDevice(); - - await aliceCompleteProm; - expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UserDeclined); - } finally { - aliceTransport.cleanup(); - bobTransport.cleanup(); - } - }); + const codePromise = aliceRz.generateCode(); + await httpBackend.flush(''); - it("no protocol available", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - try { - // alice is signing in initiates and generates a code - const aliceOnFailure = jest.fn(); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, undefined, aliceOnFailure); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const aliceStartProm = aliceRz.startAfterShowingCode(); + await codePromise; - // bob is already signed in and scans the code - const bob = makeMockClient({ userId: "bob", deviceId: "BOB", msc3882Enabled: false }); - const { - channel: bobEcdh, - intent: aliceIntentAsSeenByBob, - } = await buildChannelFromCode(aliceRz.code!, () => {}, fetchFn); - // we override the channel to set to dummy transport: - (bobEcdh as unknown as MSC3903ECDHv1RendezvousChannel).transport = bobTransport; - const bobRz = new MSC3906Rendezvous(bobEcdh, bob); - const bobStartProm = bobRz.startAfterScanningCode(aliceIntentAsSeenByBob); + expect(typeof aliceRz.code).toBe('string'); - // check that the two sides are talking to each other with same checksum - const aliceChecksum = await aliceStartProm; - const bobChecksum = await bobStartProm; - expect(bobChecksum).toBeUndefined(); - expect(aliceChecksum).toBeUndefined(); + const code = JSON.parse(aliceRz.code) as RendezvousCode; - expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm); - // await aliceRz.completeLoginOnNewDevice(); - } finally { - aliceTransport.cleanup(); - bobTransport.cleanup(); - } + expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); + expect(code.rendezvous.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous.transport.type).toEqual("http.v1"); + expect((code.rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) + .toEqual("https://fallbackserver/rz/123"); }); }); }); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 3bcd2a505e0..9e8c345a1e4 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -16,9 +16,24 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; +import type { MatrixClient } from "../../../src"; import { RendezvousFailureReason } from "../../../src/rendezvous"; import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; +function makeMockClient(opts: { userId: string, deviceId: string, msc3886Enabled: boolean}): MatrixClient { + return { + doesServerSupportUnstableFeature(feature: string) { + return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886"); + }, + getUserId() { return opts.userId; }, + getDeviceId() { return opts.deviceId; }, + requestLoginToken() { + return Promise.resolve({ login_token: "token" }); + }, + baseUrl: "https://example.com", + } as unknown as MatrixClient; +} + describe("SimpleHttpRendezvousTransport", function() { let httpBackend: MockHttpBackend; let fetchFn: typeof global.fetch; @@ -29,14 +44,20 @@ describe("SimpleHttpRendezvousTransport", function() { }); async function postAndCheckLocation( + msc3886Enabled: boolean, fallbackRzServer: string, locationResponse: string, expectedFinalLocation: string, ) { - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fallbackRzServer, fetchFn }); + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn }); { // initial POST + const expectedPostLocation = msc3886Enabled ? + `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous` : + fallbackRzServer; + const prom = simpleHttpTransport.send("application/json", {}); - httpBackend.when("POST", fallbackRzServer).response = { + httpBackend.when("POST", expectedPostLocation).response = { body: null, response: { statusCode: 201, @@ -66,12 +87,15 @@ describe("SimpleHttpRendezvousTransport", function() { } } it("should throw an error when no server available", function() { - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ fetchFn }); + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn }); expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -90,19 +114,45 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST with absolute path response", async function() { - await postAndCheckLocation("https://fallbackserver/rz", "/123", "https://fallbackserver/123"); + await postAndCheckLocation( + false, + "https://fallbackserver/rz", + "/123", + "https://fallbackserver/123", + ); + }); + + it("POST to built-in MSC3886 implementation", async function() { + await postAndCheckLocation( + true, + "https://fallbackserver/rz", + "123", + "https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123", + ); }); - it("POST with relative path response", async function() { - await postAndCheckLocation("https://fallbackserver/rz", "123", "https://fallbackserver/rz/123"); + it("POST with relative path response including parent", async function() { + await postAndCheckLocation( + false, + "https://fallbackserver/rz/abc", + "../xyz/123", + "https://fallbackserver/rz/xyz/123", + ); }); it("POST with relative path response including parent", async function() { - await postAndCheckLocation("https://fallbackserver/rz/abc", "../xyz/123", "https://fallbackserver/rz/xyz/123"); + await postAndCheckLocation( + false, + "https://fallbackserver/rz/abc", + "../xyz/123", + "https://fallbackserver/rz/xyz/123", + ); }); it("POST to follow 307 to other server", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -131,7 +181,9 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and GET", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -187,7 +239,9 @@ describe("SimpleHttpRendezvousTransport", function() { }); it("POST and PUTs", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); @@ -243,52 +297,10 @@ describe("SimpleHttpRendezvousTransport", function() { } }); - it("init with URI", async function() { - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ - rendezvousUri: "https://server/rz/123", - fetchFn, - }); - { - const prom = simpleHttpTransport.receive(); - httpBackend.when("GET", "https://server/rz/123").response = { - body: { foo: "baa" }, - response: { - statusCode: 200, - headers: { - "content-type": "application/json", - "etag": "aaa", - }, - }, - }; - await httpBackend.flush(''); - expect(await prom).toEqual({ foo: "baa" }); - } - }); - - it("init from HS", async function() { - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ - rendezvousUri: "https://server/rz/123", - fetchFn, - }); - { - const prom = simpleHttpTransport.receive(); - httpBackend.when("GET", "https://server/rz/123").response = { - body: { foo: "baa" }, - response: { - statusCode: 200, - headers: { - "content-type": "application/json", - "etag": "aaa", - }, - }, - }; - await httpBackend.flush(''); - expect(await prom).toEqual({ foo: "baa" }); - } - }); - it("POST and DELETE", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index e2394e66736..93b3a6fe186 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -83,6 +83,7 @@ async function importKey(key: Uint8Array): Promise { /** * 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 MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 9f0f2967225..1028df97ef5 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -14,72 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RendezvousFailureListener, RendezvousFailureReason } from './cancellationReason'; -import { RendezvousChannel } from './channel'; -import { RendezvousCode, RendezvousIntent } from './code'; -import { RendezvousError } from './error'; -import { MSC3886SimpleHttpRendezvousTransport, MSC3886SimpleHttpRendezvousTransportDetails } from './transports'; -import { decodeBase64 } from '../crypto/olmlib'; -import { MSC3903ECDHv1RendezvousChannel, ECDHv1RendezvousCode, SecureRendezvousChannelAlgorithm } from './channels'; -import { logger } from '../logger'; - export * from './code'; export * from './cancellationReason'; export * from './transport'; export * from './channel'; export * from './rendezvous'; - -/** - * Attempts to parse the given code as a rendezvous and return a channel and transport. - * @param code The code to parse. - * @param onCancelled the cancellation listener to use for the transport and secure channel. - * @returns The channel and intent of the generatoer - */ -export async function buildChannelFromCode( - code: string, - onFailure: RendezvousFailureListener, - fetchFn?: typeof global.fetch, -): Promise<{ channel: RendezvousChannel, intent: RendezvousIntent }> { - let parsed: RendezvousCode; - try { - parsed = JSON.parse(code) as RendezvousCode; - } catch (err) { - throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); - } - - const { intent, rendezvous } = parsed; - - if (rendezvous?.transport.type !== 'http.v1') { - throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedTransport); - } - - const transportDetails = rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails; - - if (typeof transportDetails.uri !== 'string') { - throw new RendezvousError('Invalid code', RendezvousFailureReason.InvalidCode); - } - - if (!intent || !Object.values(RendezvousIntent).includes(intent)) { - throw new RendezvousError('Invalid intent', RendezvousFailureReason.InvalidCode); - } - - const transport = new MSC3886SimpleHttpRendezvousTransport({ - onFailure, - rendezvousUri: transportDetails.uri, - fetchFn, - }); - - if (rendezvous?.algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1) { - throw new RendezvousError('Unsupported transport', RendezvousFailureReason.UnsupportedAlgorithm); - } - - const ecdhCode = parsed as ECDHv1RendezvousCode; - - const theirPublicKey = decodeBase64(ecdhCode.rendezvous.key); - - logger.info(`Building ECDHv1 rendezvous via HTTP from: ${code}`); - return { - channel: new MSC3903ECDHv1RendezvousChannel(transport, theirPublicKey, onFailure), - intent, - }; -} +export * from './error'; diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index ed0221bbef9..d6016f9edb6 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -17,13 +17,12 @@ limitations under the License. import { RendezvousChannel } from "."; import { LoginTokenPostResponse } from "../@types/auth"; import { MatrixClient } from "../client"; -import { CrossSigningInfo, requestKeysDuringVerification } from "../crypto/CrossSigning"; +import { CrossSigningInfo } from "../crypto/CrossSigning"; import { DeviceInfo } from "../crypto/deviceinfo"; import { IAuthData } from "../interactive-auth"; import { logger } from "../logger"; -import { createClient } from "../matrix"; import { sleep } from "../utils"; -import { RendezvousFailureReason } from "./cancellationReason"; +import { RendezvousFailureListener, RendezvousFailureReason } from "./cancellationReason"; import { RendezvousIntent } from "./code"; enum PayloadType { @@ -32,23 +31,22 @@ enum PayloadType { Progress = 'm.login.progress', } +/** + * 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 cli?: MatrixClient; private newDeviceId?: string; private newDeviceKey?: string; - private ourIntent: RendezvousIntent; + private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; public code?: string; constructor( public channel: RendezvousChannel, - cli?: MatrixClient, - public onFailure?: (reason: RendezvousFailureReason) => void, - ) { - this.cli = cli; - this.ourIntent = this.isNewDevice ? - RendezvousIntent.LOGIN_ON_NEW_DEVICE : - RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; - } + public client: MatrixClient, + public onFailure?: RendezvousFailureListener, + ) {} async generateCode(): Promise { if (this.code) { @@ -58,129 +56,38 @@ export class MSC3906Rendezvous { this.code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); } - private get isNewDevice(): boolean { - return !this.cli; - } - - private async areIntentsIncompatible(theirIntent: RendezvousIntent): Promise { - const incompatible = theirIntent === this.ourIntent; - - if (incompatible) { - await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); - await this.channel.cancel( - this.isNewDevice ? - RendezvousFailureReason.OtherDeviceNotSignedIn : - RendezvousFailureReason.OtherDeviceAlreadySignedIn, - ); - } - - return incompatible; - } - async startAfterShowingCode(): Promise { - return this.start(); - } - - async startAfterScanningCode(theirIntent: RendezvousIntent): Promise { - return this.start(theirIntent); - } - - private async start(theirIntent?: RendezvousIntent): Promise { - const didScan = !!theirIntent; - const checksum = await this.channel.connect(); logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); - if (didScan) { - if (await this.areIntentsIncompatible(theirIntent)) { - // a m.login.finish event is sent as part of areIntentsIncompatible + { + const res = await this.channel.receive(); + if (res?.intent !== RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); + await this.cancel(RendezvousFailureReason.OtherDeviceAlreadySignedIn); return undefined; } } - if (this.cli) { - if (didScan) { - await this.channel.receive(); // wait for ack - } - - // determine available protocols - if (!(await this.cli.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { - logger.info("Server doesn't support MSC3882"); - await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); - await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); - return undefined; - } - - await this.send({ type: PayloadType.Progress, protocols: ['login_token'] }); - - logger.info('Waiting for other device to chose protocol'); - const { type, protocol, outcome } = await this.channel.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') { - await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); - return undefined; - } - } else { - if (!didScan) { - logger.info("Sending ack"); - await this.send({ type: PayloadType.Progress }); - } - - logger.info("Waiting for protocols"); - const { protocols } = await this.channel.receive(); - - if (!Array.isArray(protocols) || !protocols.includes('login_token')) { - await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); - await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); - return undefined; - } - - await this.send({ type: PayloadType.Progress, protocol: "login_token" }); + // determine available protocols + if (!(await this.client.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { + logger.info("Server doesn't support MSC3882"); + await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); + return undefined; } - return checksum; - } + await this.send({ type: PayloadType.Progress, protocols: ['login_token'] }); - private async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { - await this.channel.send({ type, ...payload }); - } - - async completeLoginOnNewDevice(): Promise<{ - userId: string; - deviceId: string; - accessToken: string; - homeserverUrl: string; - } | undefined> { - logger.info('Waiting for login_token'); - - // eslint-disable-next-line camelcase - const { type, login_token: token, outcome, homeserver } = await this.channel.receive(); + logger.info('Waiting for other device to chose protocol'); + const { type, protocol, outcome } = await this.channel.receive(); if (type === PayloadType.Finish) { + // new device decided not to complete switch (outcome ?? '') { case 'unsupported': - await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); - break; - case 'declined': - await this.cancel(RendezvousFailureReason.UserDeclined); + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); break; default: await this.cancel(RendezvousFailureReason.Unknown); @@ -188,69 +95,21 @@ export class MSC3906Rendezvous { return undefined; } - if (!homeserver) { - throw new Error("No homeserver returned"); - } - // eslint-disable-next-line camelcase - if (!token) { - throw new Error("No login token returned"); - } - - const client = createClient({ - baseUrl: homeserver, - }); - - const { device_id: deviceId, user_id: userId, access_token: accessToken } = - await client.login("m.login.token", { token }); - - return { - userId, - deviceId, - accessToken, - homeserverUrl: homeserver, - }; - } - - async completeVerificationOnNewDevice(client: MatrixClient): Promise { - await this.send({ - type: PayloadType.Progress, - outcome: 'success', - device_id: client.getDeviceId(), - device_key: client.getDeviceEd25519Key(), - }); - - // await confirmation of verification - const { - verifying_device_id: verifyingDeviceId, - master_key: masterKey, - verifying_device_key: verifyingDeviceKey, - } = await this.channel.receive(); - - if (!verifyingDeviceId || !verifyingDeviceKey) { - logger.warn("No verifying_device_id or verifying_device_key received"); - return; + if (type !== PayloadType.Progress) { + await this.cancel(RendezvousFailureReason.Unknown); + return undefined; } - const userId = client.getUserId()!; - const verifyingDeviceFromServer = - client.crypto?.deviceList.getStoredDevice(userId, verifyingDeviceId); - - if (verifyingDeviceFromServer?.getFingerprint() !== verifyingDeviceKey) { - logger.warn(`Verifying device ${verifyingDeviceId} doesn't match: ${verifyingDeviceFromServer}`); - return; + if (protocol !== 'login_token') { + await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; } - // set other device as verified - logger.info(`Setting device ${verifyingDeviceId} as verified`); - await client.setDeviceVerified(userId, verifyingDeviceId, true); - if (masterKey) { - // set master key as trusted - await client.setDeviceVerified(userId, masterKey, true); - } + return checksum; + } - // request secrets from the verifying device - logger.info(`Requesting secrets from ${verifyingDeviceId}`); - await requestKeysDuringVerification(client, userId, verifyingDeviceId); + private async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { + await this.channel.send({ type, ...payload }); } async declineLoginOnExistingDevice() { @@ -259,15 +118,9 @@ export class MSC3906Rendezvous { } async confirmLoginOnExistingDevice(): Promise { - if (!this.cli) { - throw new Error('No client set'); - } - - const client = this.cli; - logger.info("Requesting login token"); - const loginTokenResponse = await client.requestLoginToken(); + const loginTokenResponse = await this.client.requestLoginToken(); if (typeof (loginTokenResponse as IAuthData).session === 'string') { // TODO: handle UIA response @@ -277,7 +130,7 @@ export class MSC3906Rendezvous { const { login_token } = loginTokenResponse as LoginTokenPostResponse; // eslint-disable-next-line camelcase - await this.send({ type: PayloadType.Progress, login_token, homeserver: client.baseUrl }); + await this.send({ type: PayloadType.Progress, login_token, homeserver: this.client.baseUrl }); logger.info('Waiting for outcome'); const res = await this.channel.receive(); @@ -297,11 +150,7 @@ export class MSC3906Rendezvous { } private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo) { - if (!this.cli) { - throw new Error('No client set'); - } - - if (!this.cli.crypto) { + if (!this.client.crypto) { throw new Error('Crypto not available on client'); } @@ -316,26 +165,26 @@ export class MSC3906Rendezvous { ); } - const userId = this.cli.getUserId(); + 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.cli.crypto.setDeviceVerification( + const info = await this.client.crypto.setDeviceVerification( userId, this.newDeviceId, true, false, true, ); - const masterPublicKey = this.cli.crypto.crossSigningInfo.getId('master'); + const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master'); await this.send({ type: PayloadType.Finish, outcome: 'verified', - verifying_device_id: this.cli.getDeviceId(), - verifying_device_key: this.cli.getDeviceEd25519Key(), + verifying_device_id: this.client.getDeviceId(), + verifying_device_key: this.client.getDeviceEd25519Key(), master_key: masterPublicKey, }); @@ -351,24 +200,19 @@ export class MSC3906Rendezvous { logger.info("No new device key to sign"); return undefined; } - const client = this.cli; - if (!client) { - throw new Error('No client set'); - } - - if (!client.crypto) { + if (!this.client.crypto) { throw new Error('Crypto not available on client'); } - const userId = client.getUserId(); + const userId = this.client.getUserId(); if (!userId) { throw new Error('No user ID set'); } { - const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); + const deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.verifyAndCrossSignDevice(deviceInfo); @@ -379,7 +223,7 @@ export class MSC3906Rendezvous { await sleep(timeout); { - const deviceInfo = client.crypto.getStoredDevice(userId, this.newDeviceId); + const deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); if (deviceInfo) { return await this.verifyAndCrossSignDevice(deviceInfo); @@ -389,10 +233,6 @@ export class MSC3906Rendezvous { throw new Error('Device not online within timeout'); } - async userCancelled(): Promise { - this.cancel(RendezvousFailureReason.UserCancelled); - } - async cancel(reason: RendezvousFailureReason) { this.onFailure?.(reason); await this.channel.cancel(reason); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 8dc01c14080..0e776a71a3b 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -29,6 +29,7 @@ export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousT /** * 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 { ready = false; @@ -37,32 +38,25 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport private etag?: string; private expiresAt?: Date; public onFailure?: RendezvousFailureListener; - private client?: MatrixClient; - private hsUrl?: string; + private client: MatrixClient; private fallbackRzServer?: string; private fetchFn?: typeof global.fetch; constructor({ onFailure, client, - hsUrl, fallbackRzServer, - rendezvousUri, fetchFn, }: { fetchFn?: typeof global.fetch; onFailure?: RendezvousFailureListener; - client?: MatrixClient; - hsUrl?: string; + client: MatrixClient; fallbackRzServer?: string; - rendezvousUri?: string; }) { this.fetchFn = fetchFn; this.onFailure = onFailure; this.client = client; - this.hsUrl = hsUrl; this.fallbackRzServer = fallbackRzServer; - this.uri = rendezvousUri; this.ready = !!this.uri; } @@ -85,20 +79,12 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } private async getPostEndpoint(): Promise { - if (!this.client && this.hsUrl) { - this.client = new MatrixClient({ - baseUrl: this.hsUrl, - }); - } - - if (this.client) { - 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); + 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; From c92a5f0fd84f22abec13804d79421fe2efb5d5d8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 18:28:15 +0100 Subject: [PATCH 38/74] Strict linting --- spec/unit/rendezvous/rendezvous.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 461724f4c85..bdf9f380b61 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -95,12 +95,12 @@ describe("Rendezvous", function() { expect(typeof aliceRz.code).toBe('string'); - const code = JSON.parse(aliceRz.code) as RendezvousCode; + const code = JSON.parse(aliceRz.code!) as RendezvousCode; expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); - expect(code.rendezvous.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); - expect(code.rendezvous.transport.type).toEqual("http.v1"); - expect((code.rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) + expect(code.rendezvous?.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous?.transport.type).toEqual("http.v1"); + expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) .toEqual("https://fallbackserver/rz/123"); }); }); From f1324b5bcb9efea098a6877ff7b9fa564a38a4e9 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 22:28:57 +0100 Subject: [PATCH 39/74] Additional test cases --- spec/unit/rendezvous/rendezvous.spec.ts | 510 ++++++++++++++++-- .../rendezvous/simpleHttpTransport.spec.ts | 116 +++- src/rendezvous/rendezvous.ts | 9 - 3 files changed, 590 insertions(+), 45 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index bdf9f380b61..4cd6b658f07 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -17,19 +17,29 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; -import { MSC3906Rendezvous, RendezvousCode, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; +import { ECDHv1RendezvousCode, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; import { MatrixClient } from "../../../src"; import { MSC3886SimpleHttpRendezvousTransport, MSC3886SimpleHttpRendezvousTransportDetails, } from "../../../src/rendezvous/transports"; +import { DummyTransport } from "./DummyTransport"; +import { decodeBase64 } from "../../../src/crypto/olmlib"; +import { logger } from "../../../src/logger"; +import { DeviceInfo } from "../../../src/crypto/deviceinfo"; function makeMockClient(opts: { userId: string; deviceId: string; + deviceKey?: string; msc3882Enabled: boolean; msc3886Enabled: boolean; + devices?: Record>; + verificationFunction?: ( + userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean, + ) => void; + crossSigningIds?: Record; }): MatrixClient { return { doesServerSupportUnstableFeature(feature: string) { @@ -41,10 +51,22 @@ function makeMockClient(opts: { }, getUserId() { return opts.userId; }, getDeviceId() { return opts.deviceId; }, + getDeviceEd25519Key() { return opts.deviceKey; }, requestLoginToken() { return Promise.resolve({ login_token: "token" }); }, baseUrl: "https://example.com", + crypto: { + getStoredDevice(userId: string, deviceId: string) { + return opts.devices?.[deviceId] ?? null; + }, + setDeviceVerification: opts.verificationFunction, + crossSigningInfo: { + getId(key: string) { + return opts.crossSigningIds?.[key]; + }, + }, + }, } as unknown as MatrixClient; } @@ -55,53 +77,471 @@ describe("Rendezvous", function() { let httpBackend: MockHttpBackend; let fetchFn: typeof global.fetchFn; + let transports: DummyTransport[]; beforeEach(function() { httpBackend = new MockHttpBackend(); fetchFn = httpBackend.fetchFn as typeof global.fetch; + transports = []; }); - describe("end-to-end", function() { - it("generate", async function() { - const alice = makeMockClient({ - userId: "@alice:example.com", - deviceId: "DEVICEID", - msc3886Enabled: false, - msc3882Enabled: true, - }); - httpBackend.when("POST", "https://fallbackserver/rz").response = { - body: null, - response: { - statusCode: 201, - headers: { - location: "https://fallbackserver/rz/123", - }, + afterEach(function() { + transports.forEach(x => x.cleanup()); + }); + + it("generate and cancel", async function() { + const alice = makeMockClient({ + userId: "@alice:example.com", + deviceId: "DEVICEID", + msc3886Enabled: false, + msc3882Enabled: true, + }); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", }, - }; - const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({ - client: alice, - fallbackRzServer: "https://fallbackserver/rz", - fetchFn, + }, + }; + const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({ + client: alice, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + + expect(aliceRz.code).toBeUndefined(); + + const codePromise = aliceRz.generateCode(); + await httpBackend.flush(''); + + await aliceRz.generateCode(); + + expect(typeof aliceRz.code).toBe('string'); + + await codePromise; + + const code = JSON.parse(aliceRz.code!) as RendezvousCode; + + expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); + expect(code.rendezvous?.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous?.transport.type).toEqual("http.v1"); + expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) + .toEqual("https://fallbackserver/rz/123"); + + httpBackend.when("DELETE", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 204, + headers: {}, + }, + }; + + const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined); + await httpBackend.flush(''); + expect(cancelPromise).resolves.toBeUndefined(); + httpBackend.verifyNoOutstandingExpectation(); + httpBackend.verifyNoOutstandingRequests(); + + await aliceRz.close(); + }); + + it("no protocols", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: false, + msc3886Enabled: false, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); + + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); + + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); + + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.finish', + outcome: 'unsupported', }); - const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + })(); + + await aliceStartProm; + await bobStartPromise; + }); + + it("new device declines protocol", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: true, + msc3886Enabled: false, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); - expect(aliceRz.code).toBeUndefined(); + const aliceStartProm = aliceRz.startAfterShowingCode(); - const codePromise = aliceRz.generateCode(); - await httpBackend.flush(''); + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); - await codePromise; + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); - expect(typeof aliceRz.code).toBe('string'); + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.progress', + protocols: ['login_token'], + }); + + await bobEcdh.send({ type: 'm.login.finish', outcome: 'unsupported' }); + })(); + + await aliceStartProm; + await bobStartPromise; + + expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm); + }); - const code = JSON.parse(aliceRz.code!) as RendezvousCode; + it("new device declines protocol", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; - expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); - expect(code.rendezvous?.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); - expect(code.rendezvous?.transport.type).toEqual("http.v1"); - expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) - .toEqual("https://fallbackserver/rz/123"); + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: true, + msc3886Enabled: false, }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); + + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); + + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); + + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.progress', + protocols: ['login_token'], + }); + + await bobEcdh.send({ type: 'm.login.progress', protocol: 'bad protocol' }); + })(); + + await aliceStartProm; + await bobStartPromise; + + expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm); + }); + + it("decline on existing device", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: true, + msc3886Enabled: false, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); + + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); + + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); + + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.progress', + protocols: ['login_token'], + }); + + await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + })(); + + await aliceStartProm; + await bobStartPromise; + + await aliceRz.declineLoginOnExistingDevice(); + const loginToken = await bobEcdh.receive(); + expect(loginToken).toEqual({ type: 'm.login.finish', outcome: 'declined' }); + }); + + it("approve on existing device + no verification", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: true, + msc3886Enabled: false, + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); + + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); + + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); + + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.progress', + protocols: ['login_token'], + }); + + await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + })(); + + await aliceStartProm; + await bobStartPromise; + + const confirmProm = aliceRz.confirmLoginOnExistingDevice(); + + const bobCompleteProm = (async () => { + const loginToken = await bobEcdh.receive(); + expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl }); + await bobEcdh.send({ type: 'm.login.finish', outcome: 'success' }); + })(); + + await confirmProm; + await bobCompleteProm; + }); + + it("approve on existing device + verification", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); + const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + transports.push(aliceTransport, bobTransport); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // const info = await this.client.crypto.setDeviceVerification( + // userId, + // this.newDeviceId, + // true, false, true, + // ); + // alice is aleady signs in and generates a code + const aliceOnFailure = jest.fn(); + const aliceVerification = jest.fn(); + const alice = makeMockClient({ + userId: "alice", + deviceId: "ALICE", + msc3882Enabled: true, + msc3886Enabled: false, + devices: { + BOB: { + getFingerprint: () => "bbbb", + }, + }, + deviceKey: 'aaaa', + verificationFunction: aliceVerification, + crossSigningIds: { + master: 'mmmmm', + } + }); + const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); + const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); + aliceTransport.onCancelled = aliceOnFailure; + await aliceRz.generateCode(); + const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode; + + expect(code.rendezvous.key).toBeDefined(); + + const aliceStartProm = aliceRz.startAfterShowingCode(); + + // bob is try to sign in and scans the code + const bobOnFailure = jest.fn(); + const bobEcdh = new MSC3903ECDHv1RendezvousChannel( + bobTransport, + decodeBase64(code.rendezvous.key), // alice's public key + bobOnFailure, + ); + + const bobStartPromise = (async () => { + const bobChecksum = await bobEcdh.connect(); + logger.info(`Bob checksums is ${bobChecksum} now sending intent`); + // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE }); + + // wait for protocols + logger.info('Bob waiting for protocols'); + const protocols = await bobEcdh.receive(); + + logger.info(`Bob protocols: ${JSON.stringify(protocols)}`); + + expect(protocols).toEqual({ + type: 'm.login.progress', + protocols: ['login_token'], + }); + + await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + })(); + + await aliceStartProm; + await bobStartPromise; + + const confirmProm = aliceRz.confirmLoginOnExistingDevice(); + + const bobLoginProm = (async () => { + const loginToken = await bobEcdh.receive(); + expect(loginToken).toEqual({ type: 'm.login.progress', login_token: 'token', homeserver: alice.baseUrl }); + await bobEcdh.send({ type: 'm.login.finish', outcome: 'success', device_id: 'BOB', device_key: 'bbbb' }); + })(); + + expect(await confirmProm).toEqual('BOB'); + await bobLoginProm; + + const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice(); + + const bobVerifyProm = (async () => { + const verified = await bobEcdh.receive(); + expect(verified).toEqual({ + type: 'm.login.finish', + outcome: 'verified', + verifying_device_id: 'ALICE', + verifying_device_key: 'aaaa', + master_key: 'mmmmm', + }); + })(); + + await verifyProm; + await bobVerifyProm; }); }); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 9e8c345a1e4..8cb51bd8f71 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -69,6 +69,9 @@ describe("SimpleHttpRendezvousTransport", function() { await httpBackend.flush(''); await prom; } + const details = await simpleHttpTransport.details(); + expect(details.uri).toBe(expectedFinalLocation); + { // first GET without etag const prom = simpleHttpTransport.receive(); httpBackend.when("GET", expectedFinalLocation).response = { @@ -89,7 +92,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("should throw an error when no server available", function() { const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn }); - expect(simpleHttpTransport.send("application/json", {})).rejects.toThrow("Invalid rendezvous URI"); + expect(simpleHttpTransport.send("application/json", {})).rejects.toThrowError("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { @@ -113,6 +116,25 @@ describe("SimpleHttpRendezvousTransport", function() { expect(await prom).toStrictEqual(undefined); }); + it("POST with no location", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + const prom = simpleHttpTransport.send("application/json", {}); + expect(prom).rejects.toThrowError(); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: {}, + }, + }; + await httpBackend.flush(''); + }); + it("POST with absolute path response", async function() { await postAndCheckLocation( false, @@ -334,4 +356,96 @@ describe("SimpleHttpRendezvousTransport", function() { await prom; } }); + + it("details before ready", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + expect(simpleHttpTransport.details()).rejects.toThrowError(); + }); + + it("send after cancelled", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined); + expect(simpleHttpTransport.send("application/json", 'asd')).resolves.toBeUndefined(); + }); + + it("receive before ready", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + }); + expect(simpleHttpTransport.receive()).rejects.toThrowError(); + }); + + it("404 failure callback", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const onFailure = jest.fn(); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + onFailure, + }); + + expect(simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" }))).resolves.toBeUndefined(); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 404, + headers: {}, + }, + }; + await httpBackend.flush('', 1); + expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown); + }); + + it("404 failure callback mapped to expired", async function() { + const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); + const onFailure = jest.fn(); + const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ + client, + fallbackRzServer: "https://fallbackserver/rz", + fetchFn, + onFailure, + }); + + { // initial POST + const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + httpBackend.when("POST", "https://fallbackserver/rz").response = { + body: null, + response: { + statusCode: 201, + headers: { + location: "https://fallbackserver/rz/123", + expires: "Thu, 01 Jan 1970 00:00:00 GMT", + }, + }, + }; + await httpBackend.flush(''); + await prom; + } + { // GET with 404 to simulate expiry + expect(simpleHttpTransport.receive()).resolves.toBeUndefined(); + httpBackend.when("GET", "https://fallbackserver/rz/123").response = { + body: { foo: "baa" }, + response: { + statusCode: 404, + headers: {}, + }, + }; + await httpBackend.flush(''); + expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired); + } + }); }); diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index d6016f9edb6..7912fef9ba8 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -61,15 +61,6 @@ export class MSC3906Rendezvous { logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); - { - const res = await this.channel.receive(); - if (res?.intent !== RendezvousIntent.LOGIN_ON_NEW_DEVICE) { - await this.send({ type: PayloadType.Finish, intent: this.ourIntent }); - await this.cancel(RendezvousFailureReason.OtherDeviceAlreadySignedIn); - return undefined; - } - } - // determine available protocols if (!(await this.client.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { logger.info("Server doesn't support MSC3882"); From f8a738d68acec9747c4f975e253cb3519b0569e7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sat, 15 Oct 2022 22:30:47 +0100 Subject: [PATCH 40/74] Lint --- spec/unit/rendezvous/rendezvous.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 4cd6b658f07..199921b3d06 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -475,7 +475,7 @@ describe("Rendezvous", function() { verificationFunction: aliceVerification, crossSigningIds: { master: 'mmmmm', - } + }, }); const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); From 3f53046059ee4b9aabc3170d63406ff12a10a2d4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 10:50:24 +0100 Subject: [PATCH 41/74] additional test cases and remove some code smells --- spec/unit/rendezvous/ecdh.spec.ts | 183 +++++++++++++----- src/rendezvous/channels/ecdhV1.ts | 9 - .../transports/simpleHttpTransport.ts | 10 +- 3 files changed, 140 insertions(+), 62 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index fce579d02a6..12d834a7c2f 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -25,61 +25,158 @@ import { setCrypto } from '../../../src/utils'; describe('ECDHv1', function() { beforeAll(async function() { - setCrypto(crypto); await global.Olm.init(); }); - it("initiator wants to sign in", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; + describe('with crypto', () => { + beforeEach(async function() { + setCrypto(crypto); + }); + it("initiator wants to sign in", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); + expect(aliceChecksum).toEqual(bobChecksum); - expect(aliceChecksum).toEqual(bobChecksum); + const message = "hello world"; + await alice.send(message); + const bobReceive = await bob.receive(); + expect(bobReceive).toEqual(message); - const message = "hello world"; - await alice.send(message); - const bobReceive = await bob.receive(); - expect(bobReceive).toEqual(message); + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); - }); + it("initiator wants to reciprocate", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - it("initiator wants to reciprocate", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + expect(aliceChecksum).toEqual(bobChecksum); - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); + const message = "hello world"; + await bob.send(message); + const aliceReceive = await alice.receive(); + expect(aliceReceive).toEqual(message); - expect(aliceChecksum).toEqual(bobChecksum); + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); - const message = "hello world"; - await bob.send(message); - const aliceReceive = await alice.receive(); - expect(aliceReceive).toEqual(message); + it("double connect", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + expect(alice.connect()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("closed", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + alice.close(); + + expect(alice.connect()).rejects.toThrow(); + expect(alice.send('')).rejects.toThrow(); + expect(alice.receive()).rejects.toThrow(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("require ciphertext", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); + + const bobChecksum = await bob.connect(); + const aliceChecksum = await alice.connect(); + + expect(aliceChecksum).toEqual(bobChecksum); + + // send a message without encryption + await aliceTransport.send('application/json', '{}'); + expect(bob.receive()).rejects.toThrowError(); + + await alice.cancel(RendezvousFailureReason.Unknown); + await bob.cancel(RendezvousFailureReason.Unknown); + }); + + it("ciphertext before set up", async function() { + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); + const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + aliceTransport.otherParty = bobTransport; + bobTransport.otherParty = aliceTransport; + + // alice is signing in initiates and generates a code + const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); + await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); + + await bobTransport.send('application/json', '{ ciphertext: "foo" }'); + + expect(alice.receive()).rejects.toThrowError(); + + await alice.cancel(RendezvousFailureReason.Unknown); + }); }); - it("double connect", async function() { + it("no crypto", async function() { + // simulates running in a browser without crypto support + // n.b. we can't test subtle crypto because it's not available in jsdom jest environment + setCrypto(undefined); + const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); aliceTransport.otherParty = bobTransport; @@ -90,12 +187,8 @@ describe('ECDHv1', function() { const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); - - expect(aliceChecksum).toEqual(bobChecksum); - - expect(alice.connect()).rejects.toThrow(); + expect(bob.connect()).rejects.toThrowError(); + expect(alice.connect()).rejects.toThrowError(); await alice.cancel(RendezvousFailureReason.Unknown); await bob.cancel(RendezvousFailureReason.Unknown); diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 93b3a6fe186..40bbdbb0563 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -127,7 +127,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { if (isInitiator) { // wait for the other side to send us their public key - // logger.info('Waiting for other device to send their public key'); const res = await this.receive(); if (!res) { throw new Error('No response from other device'); @@ -162,11 +161,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.aesKey = await importKey(aesKeyBytes); - // logger.debug(`Our public key: ${encodeBase64(this.ourPublicKey)}`); - // logger.debug(`Their public key: ${encodeBase64(this.theirPublicKey!)}`); - // logger.debug(`AES info: ${aesInfo}`); - // logger.debug(`AES key: ${encodeBase64(aesKeyBytes)}`); - const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); return generateDecimalSas(Array.from(rawChecksum)); } @@ -222,7 +216,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const stringifiedData = JSON.stringify(data); if (this.aesKey) { - // logger.info(`Encrypting: ${stringifiedData}`); await this.transport.send('application/json', await this.encrypt(stringifiedData)); } else { await this.transport.send('application/json', stringifiedData); @@ -271,7 +264,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const data = await this.transport.receive(); - // logger.info(`Received data: ${JSON.stringify(data)}`); if (!data) { return data; } @@ -281,7 +273,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Shared secret not set up'); } const decrypted = await this.decrypt(data); - // logger.info(`Decrypted data: ${JSON.stringify(decrypted)}`); return JSON.parse(decrypted); } else if (this.aesKey) { throw new Error('Data received but no ciphertext'); diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 0e776a71a3b..6a64ea18054 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -101,8 +101,6 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport throw new Error('Invalid rendezvous URI'); } - // logger.debug(`Sending data: ${data} to ${uri}`); - const headers: Record = { 'content-type': contentType }; if (this.etag) { headers['if-match'] = this.etag; @@ -117,8 +115,6 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } this.etag = res.headers.get("etag") ?? undefined; - // logger.debug(`Posted data to ${uri} new etag ${this.etag}`); - if (method === 'POST') { const location = res.headers.get('location'); if (!location) { @@ -146,14 +142,13 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport if (this.cancelled) { return; } - // logger.debug(`Polling: ${this.uri} after etag ${this.etag}`); + const headers: Record = {}; if (this.etag) { headers['if-none-match'] = this.etag; } const poll = await this.fetch(this.uri, { method: "GET", headers }); - // logger.debug(`Received polling response: ${poll.status} from ${this.uri}`); if (poll.status === 404) { return this.cancel(RendezvousFailureReason.Unknown); } @@ -165,7 +160,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } else if (poll.status === 200) { this.etag = poll.headers.get("etag") ?? undefined; const data = await poll.json(); - // logger.debug(`Received data: ${JSON.stringify(data)} from ${this.uri} with etag ${this.etag}`); + return data; } await sleep(1000); @@ -184,7 +179,6 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport if (this.uri && reason === RendezvousFailureReason.UserDeclined) { try { - // logger.debug(`Deleting channel: ${this.uri}`); await this.fetch(this.uri, { method: "DELETE" }); } catch (e) { logger.warn(e); From 42ad64f066df47a700fc9141a693d0706ad240d7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:18:36 +0100 Subject: [PATCH 42/74] More test cases --- spec/unit/rendezvous/rendezvous.spec.ts | 69 +++++++++++++++++++------ 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 199921b3d06..062593aba7f 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -156,7 +156,7 @@ describe("Rendezvous", function() { aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const alice = makeMockClient({ userId: "alice", @@ -210,7 +210,7 @@ describe("Rendezvous", function() { aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const alice = makeMockClient({ userId: "alice", @@ -268,7 +268,7 @@ describe("Rendezvous", function() { aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const alice = makeMockClient({ userId: "alice", @@ -326,7 +326,7 @@ describe("Rendezvous", function() { aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const alice = makeMockClient({ userId: "alice", @@ -386,7 +386,7 @@ describe("Rendezvous", function() { aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const alice = makeMockClient({ userId: "alice", @@ -446,19 +446,14 @@ describe("Rendezvous", function() { await bobCompleteProm; }); - it("approve on existing device + verification", async function() { + async function completeLogin(devices: Record>) { const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; - // const info = await this.client.crypto.setDeviceVerification( - // userId, - // this.newDeviceId, - // true, false, true, - // ); - // alice is aleady signs in and generates a code + // alice is already signs in and generates a code const aliceOnFailure = jest.fn(); const aliceVerification = jest.fn(); const alice = makeMockClient({ @@ -466,11 +461,7 @@ describe("Rendezvous", function() { deviceId: "ALICE", msc3882Enabled: true, msc3886Enabled: false, - devices: { - BOB: { - getFingerprint: () => "bbbb", - }, - }, + devices, deviceKey: 'aaaa', verificationFunction: aliceVerification, crossSigningIds: { @@ -528,6 +519,21 @@ describe("Rendezvous", function() { expect(await confirmProm).toEqual('BOB'); await bobLoginProm; + return { + aliceTransport, + aliceEcdh, + aliceRz, + bobTransport, + bobEcdh, + }; + } + + it("approve on existing device + verification", async function() { + const { bobEcdh, aliceRz } = await completeLogin({ + BOB: { + getFingerprint: () => "bbbb", + }, + }); const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice(); const bobVerifyProm = (async () => { @@ -544,4 +550,33 @@ describe("Rendezvous", function() { await verifyProm; await bobVerifyProm; }); + + it("device not online within timeout", async function() { + const { aliceRz } = await completeLogin({}); + expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(); + }); + + it("device appears online within timeout", async function() { + const devices: Record> = {}; + const { aliceRz } = await completeLogin(devices); + // device appears after 1 second + setTimeout(() => { + devices.BOB = { + getFingerprint: () => "bbbb", + }; + }, 1000); + await aliceRz.verifyNewDeviceOnExistingDevice(2000); + }); + + it("device appears online after timeout", async function() { + const devices: Record> = {}; + const { aliceRz } = await completeLogin(devices); + // device appears after 1 second + setTimeout(() => { + devices.BOB = { + getFingerprint: () => "bbbb", + }; + }, 1500); + expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(); + }); }); From 6e1a5578f8485445d648d62d93a7b5bfc18f4ec0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:20:37 +0100 Subject: [PATCH 43/74] Strict lint --- spec/unit/rendezvous/ecdh.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 12d834a7c2f..d96844003f0 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -175,7 +175,7 @@ describe('ECDHv1', function() { it("no crypto", async function() { // simulates running in a browser without crypto support // n.b. we can't test subtle crypto because it's not available in jsdom jest environment - setCrypto(undefined); + setCrypto(undefined as typeof crypto); const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); From 85929e0615f17835563ff26fa20dac791adae0c9 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:22:35 +0100 Subject: [PATCH 44/74] Strict lint --- spec/unit/rendezvous/ecdh.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index d96844003f0..6dd97ec9bff 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -175,7 +175,7 @@ describe('ECDHv1', function() { it("no crypto", async function() { // simulates running in a browser without crypto support // n.b. we can't test subtle crypto because it's not available in jsdom jest environment - setCrypto(undefined as typeof crypto); + setCrypto(undefined as unknown as typeof crypto); const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); From 150c14766d6fc7e84d4759b8bcd3ea83c40e0a8f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:41:47 +0100 Subject: [PATCH 45/74] Test case --- spec/unit/rendezvous/rendezvous.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 062593aba7f..1f0ae074887 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -579,4 +579,13 @@ describe("Rendezvous", function() { }, 1500); expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(); }); + + it("mismatched device key", async function() { + const { aliceRz } = await completeLogin({ + BOB: { + getFingerprint: () => "XXXX", + }, + }); + expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/); + }); }); From 61f50d9d3c917647ee59668af2f8bf8eac31f229 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Sun, 16 Oct 2022 11:49:54 +0100 Subject: [PATCH 46/74] Refactor to handle UIA --- spec/unit/rendezvous/rendezvous.spec.ts | 7 ++----- src/rendezvous/rendezvous.ts | 17 ++--------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 1f0ae074887..6094c09eec9 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -52,9 +52,6 @@ function makeMockClient(opts: { getUserId() { return opts.userId; }, getDeviceId() { return opts.deviceId; }, getDeviceEd25519Key() { return opts.deviceKey; }, - requestLoginToken() { - return Promise.resolve({ login_token: "token" }); - }, baseUrl: "https://example.com", crypto: { getStoredDevice(userId: string, deviceId: string) { @@ -434,7 +431,7 @@ describe("Rendezvous", function() { await aliceStartProm; await bobStartPromise; - const confirmProm = aliceRz.confirmLoginOnExistingDevice(); + const confirmProm = aliceRz.approveLoginOnExistingDevice("token"); const bobCompleteProm = (async () => { const loginToken = await bobEcdh.receive(); @@ -508,7 +505,7 @@ describe("Rendezvous", function() { await aliceStartProm; await bobStartPromise; - const confirmProm = aliceRz.confirmLoginOnExistingDevice(); + const confirmProm = aliceRz.approveLoginOnExistingDevice("token"); const bobLoginProm = (async () => { const loginToken = await bobEcdh.receive(); diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 7912fef9ba8..fd17b95e5e0 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -15,11 +15,9 @@ limitations under the License. */ import { RendezvousChannel } from "."; -import { LoginTokenPostResponse } from "../@types/auth"; import { MatrixClient } from "../client"; import { CrossSigningInfo } from "../crypto/CrossSigning"; import { DeviceInfo } from "../crypto/deviceinfo"; -import { IAuthData } from "../interactive-auth"; import { logger } from "../logger"; import { sleep } from "../utils"; import { RendezvousFailureListener, RendezvousFailureReason } from "./cancellationReason"; @@ -108,20 +106,9 @@ export class MSC3906Rendezvous { await this.send({ type: PayloadType.Finish, outcome: 'declined' }); } - async confirmLoginOnExistingDevice(): Promise { - logger.info("Requesting login token"); - - const loginTokenResponse = await this.client.requestLoginToken(); - - if (typeof (loginTokenResponse as IAuthData).session === 'string') { - // TODO: handle UIA response - throw new Error("UIA isn't supported yet"); - } - // eslint-disable-next-line camelcase - const { login_token } = loginTokenResponse as LoginTokenPostResponse; - + async approveLoginOnExistingDevice(loginToken: string): Promise { // eslint-disable-next-line camelcase - await this.send({ type: PayloadType.Progress, login_token, homeserver: this.client.baseUrl }); + await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); logger.info('Waiting for outcome'); const res = await this.channel.receive(); From ea6f7f25bb5331a528c47e8a4a96a4c3810ab77f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 16:48:23 +0100 Subject: [PATCH 47/74] Unstable prefixes --- spec/unit/rendezvous/rendezvous.spec.ts | 20 +++++++++---------- src/rendezvous/channels/index.ts | 2 +- src/rendezvous/rendezvous.ts | 7 +++++-- .../transports/simpleHttpTransport.ts | 7 +++++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 6094c09eec9..7e3514125b7 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -124,8 +124,8 @@ describe("Rendezvous", function() { const code = JSON.parse(aliceRz.code!) as RendezvousCode; expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); - expect(code.rendezvous?.algorithm).toEqual("m.rendezvous.v1.curve25519-aes-sha256"); - expect(code.rendezvous?.transport.type).toEqual("http.v1"); + expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256"); + expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1"); expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri) .toEqual("https://fallbackserver/rz/123"); @@ -246,7 +246,7 @@ describe("Rendezvous", function() { expect(protocols).toEqual({ type: 'm.login.progress', - protocols: ['login_token'], + protocols: ['org.matrix.msc3906.login_token'], }); await bobEcdh.send({ type: 'm.login.finish', outcome: 'unsupported' }); @@ -304,7 +304,7 @@ describe("Rendezvous", function() { expect(protocols).toEqual({ type: 'm.login.progress', - protocols: ['login_token'], + protocols: ['org.matrix.msc3906.login_token'], }); await bobEcdh.send({ type: 'm.login.progress', protocol: 'bad protocol' }); @@ -362,10 +362,10 @@ describe("Rendezvous", function() { expect(protocols).toEqual({ type: 'm.login.progress', - protocols: ['login_token'], + protocols: ['org.matrix.msc3906.login_token'], }); - await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' }); })(); await aliceStartProm; @@ -422,10 +422,10 @@ describe("Rendezvous", function() { expect(protocols).toEqual({ type: 'm.login.progress', - protocols: ['login_token'], + protocols: ['org.matrix.msc3906.login_token'], }); - await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' }); })(); await aliceStartProm; @@ -496,10 +496,10 @@ describe("Rendezvous", function() { expect(protocols).toEqual({ type: 'm.login.progress', - protocols: ['login_token'], + protocols: ['org.matrix.msc3906.login_token'], }); - await bobEcdh.send({ type: 'm.login.progress', protocol: 'login_token' }); + await bobEcdh.send({ type: 'm.login.progress', protocol: 'org.matrix.msc3906.login_token' }); })(); await aliceStartProm; diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index 059223029dc..ad43b22066d 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -17,5 +17,5 @@ limitations under the License. export * from './ecdhV1'; export enum SecureRendezvousChannelAlgorithm { - ECDH_V1 = "m.rendezvous.v1.curve25519-aes-sha256" + ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" } diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index fd17b95e5e0..589a89d73d2 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { UnstableValue } from "matrix-events-sdk"; import { RendezvousChannel } from "."; import { MatrixClient } from "../client"; import { CrossSigningInfo } from "../crypto/CrossSigning"; @@ -29,6 +30,8 @@ enum PayloadType { Progress = 'm.login.progress', } +const LOGIN_TOKEN_PROTOCOL = new UnstableValue(undefined, "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. @@ -67,7 +70,7 @@ export class MSC3906Rendezvous { return undefined; } - await this.send({ type: PayloadType.Progress, protocols: ['login_token'] }); + 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.channel.receive(); @@ -89,7 +92,7 @@ export class MSC3906Rendezvous { return undefined; } - if (protocol !== 'login_token') { + if (!LOGIN_TOKEN_PROTOCOL.matches(protocol)) { await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); return undefined; } diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 6a64ea18054..3db8c3651d8 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -14,6 +14,8 @@ 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 } from '../cancellationReason'; @@ -21,8 +23,9 @@ import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; import { ClientPrefix } from '../../http-api'; +const TYPE = new UnstableValue(undefined, "org.matrix.msc3886.http.v1"); + export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { - type: 'http.v1'; uri: string; } @@ -66,7 +69,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } return { - type: 'http.v1', + type: TYPE.name, uri: this.uri, }; } From 493f1d8512111a52f7b5e101a8a7089c88c66e2b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 16:51:29 +0100 Subject: [PATCH 48/74] Lint --- src/rendezvous/rendezvous.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 589a89d73d2..8cc7c1cf94a 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { UnstableValue } from "matrix-events-sdk"; + import { RendezvousChannel } from "."; import { MatrixClient } from "../client"; import { CrossSigningInfo } from "../crypto/CrossSigning"; From 5db266bcfc0613a2ddc1bce018ed37840cc07b43 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 16:55:15 +0100 Subject: [PATCH 49/74] Missed due to lack of strict... --- src/rendezvous/rendezvous.ts | 2 +- src/rendezvous/transports/simpleHttpTransport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 8cc7c1cf94a..873b8036bd9 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -31,7 +31,7 @@ enum PayloadType { Progress = 'm.login.progress', } -const LOGIN_TOKEN_PROTOCOL = new UnstableValue(undefined, "org.matrix.msc3906.login_token"); +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. diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 3db8c3651d8..be67f7db17f 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -23,7 +23,7 @@ import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; import { MatrixClient } from '../../matrix'; import { ClientPrefix } from '../../http-api'; -const TYPE = new UnstableValue(undefined, "org.matrix.msc3886.http.v1"); +const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1"); export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails { uri: string; From 42b6259635d067e47ee43eb73a294fa11888c675 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:10:12 +0100 Subject: [PATCH 50/74] Test server capabilities using Feature --- spec/unit/rendezvous/rendezvous.spec.ts | 13 +++++++------ src/feature.ts | 4 ++++ src/rendezvous/rendezvous.ts | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 7e3514125b7..476a8d050ec 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -42,12 +42,13 @@ function makeMockClient(opts: { crossSigningIds?: Record; }): MatrixClient { return { - doesServerSupportUnstableFeature(feature: string) { - switch (feature) { - case "org.matrix.msc3882": return opts.msc3882Enabled; - case "org.matrix.msc3886": return opts.msc3886Enabled; - default: return false; - } + getVersions() { + return { + unstable_features: { + "org.matrix.msc3882": opts.msc3882Enabled, + "org.matrix.msc3886": opts.msc3886Enabled, + }, + }; }, getUserId() { return opts.userId; }, getDeviceId() { return opts.deviceId; }, diff --git a/src/feature.ts b/src/feature.ts index 5d7a4922406..d617093961e 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -25,6 +25,7 @@ export enum ServerSupport { export enum Feature { Thread = "Thread", ThreadUnreadNotifications = "ThreadUnreadNotifications", + LoginTokenRequest = "LoginTokenRequest", } type FeatureSupportCondition = { @@ -41,6 +42,9 @@ const featureSupportResolver: Record = { unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"], matrixVersion: "v1.4", }, + [Feature.LoginTokenRequest]: { + unstablePrefixes: ["org.matrix.msc3882"], + }, }; export async function buildFeatureSupportMap(versions: IServerVersions): Promise> { diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/rendezvous.ts index 873b8036bd9..521e97115e3 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/rendezvous.ts @@ -20,6 +20,7 @@ import { RendezvousChannel } 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"; import { RendezvousFailureListener, RendezvousFailureReason } from "./cancellationReason"; @@ -63,8 +64,9 @@ export class MSC3906Rendezvous { 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 (!(await this.client.doesServerSupportUnstableFeature('org.matrix.msc3882'))) { + if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { logger.info("Server doesn't support MSC3882"); await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); From 737791cd069344596fa6165fae11b27705facf0c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:12:16 +0100 Subject: [PATCH 51/74] Remove redundant assignment --- src/rendezvous/transports/simpleHttpTransport.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index be67f7db17f..bb24f5c2485 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -60,7 +60,6 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport this.onFailure = onFailure; this.client = client; this.fallbackRzServer = fallbackRzServer; - this.ready = !!this.uri; } async details(): Promise { @@ -132,7 +131,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport 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; + this.ready = true; } } From 4b00b153d64018185a2bfd8753d19b1852ec809f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:16:14 +0100 Subject: [PATCH 52/74] Refactor ro resuse generateDecimal from SAS --- src/crypto/verification/SAS.ts | 2 +- src/rendezvous/channels/ecdhV1.ts | 23 ++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 9e68e70ca14..f6bff7fc1d4 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -51,7 +51,7 @@ const newMismatchedCommitmentError = errorFactory( "m.mismatched_commitment", "Mismatched commitment", ); -function generateDecimalSas(sasBytes: number[]): [number, number, number] { +export function generateDecimalSas(sasBytes: number[]): [number, number, number] { /** * +--------+--------+--------+--------+--------+ * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 40bbdbb0563..5cddca7e86c 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -28,6 +28,7 @@ import { import { SecureRendezvousChannelAlgorithm } from '.'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; import { getCrypto } from '../../utils'; +import { generateDecimalSas } from '../../crypto/verification/SAS'; const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? (window.crypto.subtle || window.crypto.webkitSubtle) : null; @@ -40,26 +41,6 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { }; } -// The underlying algorithm is the same as: -// https://github.com/matrix-org/matrix-js-sdk/blob/75204d5cd04d67be100fca399f83b1a66ffb8118/src/crypto/verification/SAS.ts#L54-L68 -function generateDecimalSas(sasBytes: number[]): string { - /** - * +--------+--------+--------+--------+--------+ - * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | - * +--------+--------+--------+--------+--------+ - * bits: 87654321 87654321 87654321 87654321 87654321 - * \____________/\_____________/\____________/ - * 1st number 2nd number 3rd number - */ - const digits = [ - (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, - ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, - ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000, - ]; - - return digits.join('-'); -} - async function importKey(key: Uint8Array): Promise { if (getCrypto()) { return key; @@ -162,7 +143,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.aesKey = await importKey(aesKeyBytes); const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); - return generateDecimalSas(Array.from(rawChecksum)); + return generateDecimalSas(Array.from(rawChecksum)).join('-'); } private async encrypt(data: any): Promise { From a5de008a7a0e358bdfee7f82521dc266dcd49465 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:38:23 +0100 Subject: [PATCH 53/74] Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston --- src/rendezvous/transports/simpleHttpTransport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index bb24f5c2485..1617d011d60 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -169,7 +169,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - async cancel(reason: RendezvousFailureReason) { + public async cancel(reason: RendezvousFailureReason) { if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) { reason = RendezvousFailureReason.Expired; From 76ccd981ac8a866a82b6cd2fdb9e38bb6a52d97f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:38:31 +0100 Subject: [PATCH 54/74] Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston --- src/rendezvous/transports/simpleHttpTransport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index 1617d011d60..ff71bf9a6c1 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -135,7 +135,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - async receive(): Promise { + public async receive(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } From c9f9032cf7db4b44c3bc120792fbf57302dbd532 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:38:53 +0100 Subject: [PATCH 55/74] Update src/rendezvous/channels/ecdhV1.ts Co-authored-by: Travis Ralston --- src/rendezvous/channels/ecdhV1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/ecdhV1.ts index 5cddca7e86c..fa443d43b4b 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/ecdhV1.ts @@ -99,7 +99,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return rendezvous; } - async connect(): Promise { + public async connect(): Promise { if (!this.olmSAS) { throw new Error('Channel closed'); } From 4b313db498fbf86a3f638821440ef28c3157d68b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 17:40:04 +0100 Subject: [PATCH 56/74] Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston --- src/rendezvous/transports/simpleHttpTransport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/simpleHttpTransport.ts index ff71bf9a6c1..f2a77749cb1 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/simpleHttpTransport.ts @@ -92,7 +92,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport return this.fallbackRzServer; } - async send(contentType: string, data: any) { + public async send(contentType: string, data: any) { if (this.cancelled) { return; } From bacd2bfdbb781d83ad5f78fb2cb895010491a51d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 18:22:51 +0100 Subject: [PATCH 57/74] Rename files to titlecase --- .../rendezvous/simpleHttpTransport.spec.ts | 2 +- .../{rendezvous.ts => MSC3906Rendezvous.ts} | 3 +-- .../{channel.ts => RendezvousChannel.ts} | 0 src/rendezvous/RendezvousChannelAlgorithm.ts | 19 ++++++++++++++++++ src/rendezvous/{code.ts => RendezvousCode.ts} | 10 ++-------- .../{error.ts => RendezvousError.ts} | 2 +- ...onReason.ts => RendezvousFailureReason.ts} | 0 src/rendezvous/RendezvousIntent.ts | 20 +++++++++++++++++++ .../{transport.ts => RendezvousTransport.ts} | 2 +- ...1.ts => MSC3903ECDHv1RendezvousChannel.ts} | 18 ++++++++--------- src/rendezvous/channels/index.ts | 5 +---- src/rendezvous/index.ts | 14 +++++++------ ...> MSC3886SimpleHttpRendezvousTransport.ts} | 8 ++++++-- src/rendezvous/transports/index.ts | 2 +- 14 files changed, 70 insertions(+), 35 deletions(-) rename src/rendezvous/{rendezvous.ts => MSC3906Rendezvous.ts} (98%) rename src/rendezvous/{channel.ts => RendezvousChannel.ts} (100%) create mode 100644 src/rendezvous/RendezvousChannelAlgorithm.ts rename src/rendezvous/{code.ts => RendezvousCode.ts} (69%) rename src/rendezvous/{error.ts => RendezvousError.ts} (92%) rename src/rendezvous/{cancellationReason.ts => RendezvousFailureReason.ts} (100%) create mode 100644 src/rendezvous/RendezvousIntent.ts rename src/rendezvous/{transport.ts => RendezvousTransport.ts} (98%) rename src/rendezvous/channels/{ecdhV1.ts => MSC3903ECDHv1RendezvousChannel.ts} (93%) rename src/rendezvous/transports/{simpleHttpTransport.ts => MSC3886SimpleHttpRendezvousTransport.ts} (97%) diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index 8cb51bd8f71..f792aceb29c 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -18,7 +18,7 @@ import MockHttpBackend from "matrix-mock-request"; import type { MatrixClient } from "../../../src"; import { RendezvousFailureReason } from "../../../src/rendezvous"; -import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports/simpleHttpTransport"; +import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports"; function makeMockClient(opts: { userId: string, deviceId: string, msc3886Enabled: boolean}): MatrixClient { return { diff --git a/src/rendezvous/rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts similarity index 98% rename from src/rendezvous/rendezvous.ts rename to src/rendezvous/MSC3906Rendezvous.ts index 521e97115e3..f1080900bb3 100644 --- a/src/rendezvous/rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -23,8 +23,7 @@ import { DeviceInfo } from "../crypto/deviceinfo"; import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature"; import { logger } from "../logger"; import { sleep } from "../utils"; -import { RendezvousFailureListener, RendezvousFailureReason } from "./cancellationReason"; -import { RendezvousIntent } from "./code"; +import { RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from "."; enum PayloadType { Start = 'm.login.start', diff --git a/src/rendezvous/channel.ts b/src/rendezvous/RendezvousChannel.ts similarity index 100% rename from src/rendezvous/channel.ts rename to src/rendezvous/RendezvousChannel.ts diff --git a/src/rendezvous/RendezvousChannelAlgorithm.ts b/src/rendezvous/RendezvousChannelAlgorithm.ts new file mode 100644 index 00000000000..7022cd5c874 --- /dev/null +++ b/src/rendezvous/RendezvousChannelAlgorithm.ts @@ -0,0 +1,19 @@ +/* +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 RendezvousChannelAlgorithm { + ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" +} diff --git a/src/rendezvous/code.ts b/src/rendezvous/RendezvousCode.ts similarity index 69% rename from src/rendezvous/code.ts rename to src/rendezvous/RendezvousCode.ts index c77379ba6ac..59e7d0744b9 100644 --- a/src/rendezvous/code.ts +++ b/src/rendezvous/RendezvousCode.ts @@ -14,18 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SecureRendezvousChannelAlgorithm } from "./channels"; -import { RendezvousTransportDetails } from "./transport"; - -export enum RendezvousIntent { - LOGIN_ON_NEW_DEVICE = "login.start", - RECIPROCATE_LOGIN_ON_EXISTING_DEVICE = "login.reciprocate", -} +import { RendezvousChannelAlgorithm, RendezvousTransportDetails, RendezvousIntent } from "."; export interface RendezvousCode { intent: RendezvousIntent; rendezvous?: { transport: RendezvousTransportDetails; - algorithm: SecureRendezvousChannelAlgorithm; + algorithm: RendezvousChannelAlgorithm; }; } diff --git a/src/rendezvous/error.ts b/src/rendezvous/RendezvousError.ts similarity index 92% rename from src/rendezvous/error.ts rename to src/rendezvous/RendezvousError.ts index 50a2bd3e4b0..1c1f6af9c7c 100644 --- a/src/rendezvous/error.ts +++ b/src/rendezvous/RendezvousError.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RendezvousFailureReason } from "./cancellationReason"; +import { RendezvousFailureReason } from "."; export class RendezvousError extends Error { constructor(message: string, public readonly code: RendezvousFailureReason) { diff --git a/src/rendezvous/cancellationReason.ts b/src/rendezvous/RendezvousFailureReason.ts similarity index 100% rename from src/rendezvous/cancellationReason.ts rename to src/rendezvous/RendezvousFailureReason.ts diff --git a/src/rendezvous/RendezvousIntent.ts b/src/rendezvous/RendezvousIntent.ts new file mode 100644 index 00000000000..98b64ecb0ad --- /dev/null +++ b/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/src/rendezvous/transport.ts b/src/rendezvous/RendezvousTransport.ts similarity index 98% rename from src/rendezvous/transport.ts rename to src/rendezvous/RendezvousTransport.ts index a28131dc372..68251eeabbd 100644 --- a/src/rendezvous/transport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RendezvousFailureListener, RendezvousFailureReason } from "./cancellationReason"; +import { RendezvousFailureListener, RendezvousFailureReason } from "."; export interface RendezvousTransportDetails { type: string; diff --git a/src/rendezvous/channels/ecdhV1.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts similarity index 93% rename from src/rendezvous/channels/ecdhV1.ts rename to src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index fa443d43b4b..36f5145881a 100644 --- a/src/rendezvous/channels/ecdhV1.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -16,16 +16,16 @@ limitations under the License. import { SAS } from '@matrix-org/olm'; -import { RendezvousError } from '../error'; import { + RendezvousError, RendezvousCode, RendezvousIntent, RendezvousChannel, RendezvousTransportDetails, RendezvousTransport, RendezvousFailureReason, -} from '../index'; -import { SecureRendezvousChannelAlgorithm } from '.'; + RendezvousChannelAlgorithm, +} from '..'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; import { getCrypto } from '../../utils'; import { generateDecimalSas } from '../../crypto/verification/SAS'; @@ -36,7 +36,7 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? export interface ECDHv1RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1; + algorithm: RendezvousChannelAlgorithm.ECDH_V1; key: string; }; } @@ -85,11 +85,11 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Code already generated'); } - await this.send({ algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1 }); + await this.send({ algorithm: RendezvousChannelAlgorithm.ECDH_V1 }); const rendezvous: ECDHv1RendezvousCode = { "rendezvous": { - algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + algorithm: RendezvousChannelAlgorithm.ECDH_V1, key: encodeBase64(this.ourPublicKey), transport: await this.transport.details(), }, @@ -114,7 +114,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const { key, algorithm } = res; - if (algorithm !== SecureRendezvousChannelAlgorithm.ECDH_V1 || !key) { + if (algorithm !== RendezvousChannelAlgorithm.ECDH_V1 || !key) { throw new RendezvousError( 'Unsupported algorithm: ' + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, @@ -125,7 +125,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } else { // send our public key unencrypted await this.send({ - algorithm: SecureRendezvousChannelAlgorithm.ECDH_V1, + algorithm: RendezvousChannelAlgorithm.ECDH_V1, key: encodeBase64(this.ourPublicKey), }); } @@ -134,7 +134,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = SecureRendezvousChannelAlgorithm.ECDH_V1.toString(); + let aesInfo = RendezvousChannelAlgorithm.ECDH_V1.toString(); aesInfo += `|${encodeBase64(initiatorKey)}`; aesInfo += `|${encodeBase64(recipientKey)}`; diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index ad43b22066d..f1139c0629a 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -14,8 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './ecdhV1'; +export * from './MSC3903ECDHv1RendezvousChannel'; -export enum SecureRendezvousChannelAlgorithm { - ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" -} diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 1028df97ef5..a5e57155e30 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './code'; -export * from './cancellationReason'; -export * from './transport'; -export * from './channel'; -export * from './rendezvous'; -export * from './error'; +export * from './MSC3906Rendezvous'; +export * from './RendezvousChannel'; +export * from './RendezvousChannelAlgorithm'; +export * from './RendezvousCode'; +export * from './RendezvousError'; +export * from './RendezvousFailureReason'; +export * from './RendezvousIntent'; +export * from './RendezvousTransport'; diff --git a/src/rendezvous/transports/simpleHttpTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts similarity index 97% rename from src/rendezvous/transports/simpleHttpTransport.ts rename to src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index f2a77749cb1..8810226a3e3 100644 --- a/src/rendezvous/transports/simpleHttpTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -18,8 +18,12 @@ import { UnstableValue } from 'matrix-events-sdk'; import { logger } from '../../logger'; import { sleep } from '../../utils'; -import { RendezvousFailureListener, RendezvousFailureReason } from '../cancellationReason'; -import { RendezvousTransport, RendezvousTransportDetails } from '../transport'; +import { + RendezvousFailureListener, + RendezvousFailureReason, + RendezvousTransport, + RendezvousTransportDetails, +} from '..'; import { MatrixClient } from '../../matrix'; import { ClientPrefix } from '../../http-api'; diff --git a/src/rendezvous/transports/index.ts b/src/rendezvous/transports/index.ts index 8e7cadab1bc..05594da4543 100644 --- a/src/rendezvous/transports/index.ts +++ b/src/rendezvous/transports/index.ts @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './simpleHttpTransport'; +export * from './MSC3886SimpleHttpRendezvousTransport'; From e3260b0c7043326fd2aa303abfcfa8e23e1bcf40 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 18:28:27 +0100 Subject: [PATCH 58/74] Visibility modifiers --- src/rendezvous/MSC3906Rendezvous.ts | 23 ++++++++++++------- .../MSC3903ECDHv1RendezvousChannel.ts | 2 +- .../MSC3886SimpleHttpRendezvousTransport.ts | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index f1080900bb3..253410857db 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -44,13 +44,13 @@ export class MSC3906Rendezvous { private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; public code?: string; - constructor( + public constructor( public channel: RendezvousChannel, public client: MatrixClient, public onFailure?: RendezvousFailureListener, ) {} - async generateCode(): Promise { + public async generateCode(): Promise { if (this.code) { return; } @@ -58,7 +58,7 @@ export class MSC3906Rendezvous { this.code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); } - async startAfterShowingCode(): Promise { + public async startAfterShowingCode(): Promise { const checksum = await this.channel.connect(); logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); @@ -106,12 +106,12 @@ export class MSC3906Rendezvous { await this.channel.send({ type, ...payload }); } - async declineLoginOnExistingDevice() { + public async declineLoginOnExistingDevice() { logger.info('User declined sign in'); await this.send({ type: PayloadType.Finish, outcome: 'declined' }); } - async approveLoginOnExistingDevice(loginToken: string): Promise { + public async approveLoginOnExistingDevice(loginToken: string): Promise { // eslint-disable-next-line camelcase await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); @@ -174,7 +174,14 @@ export class MSC3906Rendezvous { return info; } - async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise { + /** + * 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'); } @@ -216,12 +223,12 @@ export class MSC3906Rendezvous { throw new Error('Device not online within timeout'); } - async cancel(reason: RendezvousFailureReason) { + public async cancel(reason: RendezvousFailureReason) { this.onFailure?.(reason); await this.channel.cancel(reason); } - async close() { + public async close() { await this.channel.close(); } } diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 36f5145881a..f0a3f034921 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -71,7 +71,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private ourPublicKey: Uint8Array; private aesKey?: CryptoKey | Uint8Array; - constructor( + public constructor( public transport: RendezvousTransport, private theirPublicKey?: Uint8Array, public onFailure?: (reason: RendezvousFailureReason) => void, diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index 8810226a3e3..6cd484eef4d 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -49,7 +49,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport private fallbackRzServer?: string; private fetchFn?: typeof global.fetch; - constructor({ + public constructor({ onFailure, client, fallbackRzServer, @@ -66,7 +66,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport this.fallbackRzServer = fallbackRzServer; } - async details(): Promise { + public async details(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } From 5ef415f3823524466ae49d3da5b0cbe34d4daa7a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 18:34:46 +0100 Subject: [PATCH 59/74] Resolve public mutability --- src/rendezvous/MSC3906Rendezvous.ts | 14 +++++++++----- src/rendezvous/RendezvousTransport.ts | 2 +- .../MSC3886SimpleHttpRendezvousTransport.ts | 14 +++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index 253410857db..b70564a0fbc 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -42,20 +42,24 @@ export class MSC3906Rendezvous { private newDeviceId?: string; private newDeviceKey?: string; private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; - public code?: string; + private _code?: string; public constructor( - public channel: RendezvousChannel, - public client: MatrixClient, + private channel: RendezvousChannel, + private client: MatrixClient, public onFailure?: RendezvousFailureListener, ) {} + public get() { + return this._code; + } + public async generateCode(): Promise { - if (this.code) { + if (this._code) { return; } - this.code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); } public async startAfterShowingCode(): Promise { diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index 68251eeabbd..d185b24cabb 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -27,7 +27,7 @@ export interface RendezvousTransport { /** * Ready state of the transport. This is set to true when the transport is ready to be used. */ - ready: boolean; + readonly ready: boolean; /** * Listener for cancellation events. This is called when the rendezvous is cancelled or fails. diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index 6cd484eef4d..df63ad2c9b1 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -39,15 +39,15 @@ export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousT * Note that this is UNSTABLE and may have breaking changes without notice. */ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { - ready = false; - cancelled = false; private uri?: string; private etag?: string; private expiresAt?: Date; - public onFailure?: RendezvousFailureListener; private client: MatrixClient; private fallbackRzServer?: string; private fetchFn?: typeof global.fetch; + private cancelled = false; + private _ready = false; + public onFailure?: RendezvousFailureListener; public constructor({ onFailure, @@ -66,6 +66,10 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport this.fallbackRzServer = fallbackRzServer; } + get ready() { + return this._ready; + } + public async details(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); @@ -135,7 +139,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport 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; + this._ready = true; } } @@ -180,7 +184,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } this.cancelled = true; - this.ready = false; + this._ready = false; this.onFailure?.(reason); if (this.uri && reason === RendezvousFailureReason.UserDeclined) { From c1c600f123e979e4ff723ac7c0b757c28308cebf Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 18:38:45 +0100 Subject: [PATCH 60/74] Refactor logic to reduce duplication --- src/rendezvous/MSC3906Rendezvous.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index b70564a0fbc..2347f467b51 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -205,23 +205,16 @@ export class MSC3906Rendezvous { throw new Error('No user ID set'); } - { - const deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); + let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); - if (deviceInfo) { - return await this.verifyAndCrossSignDevice(deviceInfo); - } + if (!deviceInfo) { + logger.info("Going to wait for new device to be online"); + await sleep(timeout); + deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); } - logger.info("Going to wait for new device to be online"); - await sleep(timeout); - - { - const deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); - - if (deviceInfo) { - return await this.verifyAndCrossSignDevice(deviceInfo); - } + if (deviceInfo) { + return await this.verifyAndCrossSignDevice(deviceInfo); } throw new Error('Device not online within timeout'); From 91bd0058709228eb14b793bd776cfb9e7954b1ac Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:06:08 +0100 Subject: [PATCH 61/74] Refactor to have better defined data types throughout --- spec/unit/rendezvous/DummyTransport.ts | 13 +++-- spec/unit/rendezvous/ecdh.spec.ts | 10 ++-- .../rendezvous/simpleHttpTransport.spec.ts | 26 +++++----- src/crypto/verification/SAS.ts | 17 +------ src/crypto/verification/SASDecimal.ts | 37 ++++++++++++++ src/rendezvous/MSC3906Rendezvous.ts | 2 +- src/rendezvous/RendezvousChannel.ts | 4 +- src/rendezvous/RendezvousTransport.ts | 5 +- .../MSC3903ECDHv1RendezvousChannel.ts | 49 ++++++++++--------- .../MSC3886SimpleHttpRendezvousTransport.ts | 15 +++--- 10 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 src/crypto/verification/SASDecimal.ts diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts index a2e80d00fb9..3b0931624e6 100644 --- a/spec/unit/rendezvous/DummyTransport.ts +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -27,7 +27,7 @@ export class DummyTransport implements Ren otherParty?: DummyTransport; etag?: string; lastEtagReceived?: string; - data = null; + data: object | null = null; ready = false; cancelled = false; @@ -39,10 +39,10 @@ export class DummyTransport implements Ren return Promise.resolve(this.mockDetails); } - async send(contentType: string, data: any): Promise { + async send(data: object): Promise { logger.info( `[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${ - data} of type ${contentType} where etag matches ${this.etag}`, + data} where etag matches ${this.etag}`, ); // eslint-disable-next-line no-constant-condition while (!this.cancelled) { @@ -60,18 +60,17 @@ export class DummyTransport implements Ren } } - async receive(): Promise { + async receive(): Promise { logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`); // eslint-disable-next-line no-constant-condition while (!this.cancelled) { if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) { this.lastEtagReceived = this.etag; - const data = this.data ? JSON.parse(this.data) : undefined; logger.info( `[${this.otherParty?.name}] => [${this.name}] Received data: ` + - `${JSON.stringify(data)} with etag ${this.etag}`, + `${JSON.stringify(this.data)} with etag ${this.etag}`, ); - return data; + return this.data; } logger.info(`[${this.name}] Sleeping to retry receive after etag ${ this.lastEtagReceived} as remote is ${this.etag}`); diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 6dd97ec9bff..fe7fd8e4261 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -48,7 +48,7 @@ describe('ECDHv1', function() { expect(aliceChecksum).toEqual(bobChecksum); - const message = "hello world"; + const message = { key: "xxx" }; await alice.send(message); const bobReceive = await bob.receive(); expect(bobReceive).toEqual(message); @@ -73,7 +73,7 @@ describe('ECDHv1', function() { expect(aliceChecksum).toEqual(bobChecksum); - const message = "hello world"; + const message = { key: "xxx" }; await bob.send(message); const aliceReceive = await alice.receive(); expect(aliceReceive).toEqual(message); @@ -123,7 +123,7 @@ describe('ECDHv1', function() { alice.close(); expect(alice.connect()).rejects.toThrow(); - expect(alice.send('')).rejects.toThrow(); + expect(alice.send({})).rejects.toThrow(); expect(alice.receive()).rejects.toThrow(); await alice.cancel(RendezvousFailureReason.Unknown); @@ -147,7 +147,7 @@ describe('ECDHv1', function() { expect(aliceChecksum).toEqual(bobChecksum); // send a message without encryption - await aliceTransport.send('application/json', '{}'); + await aliceTransport.send({}); expect(bob.receive()).rejects.toThrowError(); await alice.cancel(RendezvousFailureReason.Unknown); @@ -164,7 +164,7 @@ describe('ECDHv1', function() { const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - await bobTransport.send('application/json', '{ ciphertext: "foo" }'); + await bobTransport.send({ ciphertext: "foo" }); expect(alice.receive()).rejects.toThrowError(); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts index f792aceb29c..9542cb1d1cd 100644 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ b/spec/unit/rendezvous/simpleHttpTransport.spec.ts @@ -56,7 +56,7 @@ describe("SimpleHttpRendezvousTransport", function() { `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous` : fallbackRzServer; - const prom = simpleHttpTransport.send("application/json", {}); + const prom = simpleHttpTransport.send({}); httpBackend.when("POST", expectedPostLocation).response = { body: null, response: { @@ -92,7 +92,7 @@ describe("SimpleHttpRendezvousTransport", function() { it("should throw an error when no server available", function() { const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn }); - expect(simpleHttpTransport.send("application/json", {})).rejects.toThrowError("Invalid rendezvous URI"); + expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI"); }); it("POST to fallback server", async function() { @@ -102,7 +102,7 @@ describe("SimpleHttpRendezvousTransport", function() { fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); - const prom = simpleHttpTransport.send("application/json", {}); + const prom = simpleHttpTransport.send({}); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, response: { @@ -123,7 +123,7 @@ describe("SimpleHttpRendezvousTransport", function() { fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); - const prom = simpleHttpTransport.send("application/json", {}); + const prom = simpleHttpTransport.send({}); expect(prom).rejects.toThrowError(); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, @@ -178,7 +178,7 @@ describe("SimpleHttpRendezvousTransport", function() { fallbackRzServer: "https://fallbackserver/rz", fetchFn, }); - const prom = simpleHttpTransport.send("application/json", {}); + const prom = simpleHttpTransport.send({}); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, response: { @@ -210,7 +210,7 @@ describe("SimpleHttpRendezvousTransport", function() { fetchFn, }); { // initial POST - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + const prom = simpleHttpTransport.send({ foo: "baa" }); httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { expect(headers["content-type"]).toEqual("application/json"); expect(data).toEqual({ foo: "baa" }); @@ -268,7 +268,7 @@ describe("SimpleHttpRendezvousTransport", function() { fetchFn, }); { // initial POST - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + const prom = simpleHttpTransport.send({ foo: "baa" }); httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { expect(headers["content-type"]).toEqual("application/json"); expect(data).toEqual({ foo: "baa" }); @@ -285,7 +285,7 @@ describe("SimpleHttpRendezvousTransport", function() { await prom; } { // first PUT without etag - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ a: "b" })); + const prom = simpleHttpTransport.send({ a: "b" }); httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => { expect(headers["if-match"]).toBeUndefined(); expect(data).toEqual({ a: "b" }); @@ -302,7 +302,7 @@ describe("SimpleHttpRendezvousTransport", function() { await prom; } { // subsequent PUT which should have etag from previous request - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ c: "d" })); + const prom = simpleHttpTransport.send({ c: "d" }); httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => { expect(headers["if-match"]).toEqual("aaa"); }).response = { @@ -327,7 +327,7 @@ describe("SimpleHttpRendezvousTransport", function() { fetchFn, }); { // Create - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + const prom = simpleHttpTransport.send({ foo: "baa" }); httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => { expect(headers["content-type"]).toEqual("application/json"); expect(data).toEqual({ foo: "baa" }); @@ -375,7 +375,7 @@ describe("SimpleHttpRendezvousTransport", function() { fetchFn, }); await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined); - expect(simpleHttpTransport.send("application/json", 'asd')).resolves.toBeUndefined(); + expect(simpleHttpTransport.send({})).resolves.toBeUndefined(); }); it("receive before ready", async function() { @@ -398,7 +398,7 @@ describe("SimpleHttpRendezvousTransport", function() { onFailure, }); - expect(simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" }))).resolves.toBeUndefined(); + expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, response: { @@ -421,7 +421,7 @@ describe("SimpleHttpRendezvousTransport", function() { }); { // initial POST - const prom = simpleHttpTransport.send("application/json", JSON.stringify({ foo: "baa" })); + const prom = simpleHttpTransport.send({ foo: "baa" }); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, response: { diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index f6bff7fc1d4..80a0be789e7 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -32,6 +32,7 @@ import { } from './Error'; import { logger } from '../../logger'; import { IContent, MatrixEvent } from "../../models/event"; +import { generateDecimalSas } from './SASDecimal'; const START_TYPE = "m.key.verification.start"; @@ -51,22 +52,6 @@ const newMismatchedCommitmentError = errorFactory( "m.mismatched_commitment", "Mismatched commitment", ); -export function generateDecimalSas(sasBytes: number[]): [number, number, number] { - /** - * +--------+--------+--------+--------+--------+ - * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | - * +--------+--------+--------+--------+--------+ - * bits: 87654321 87654321 87654321 87654321 87654321 - * \____________/\_____________/\____________/ - * 1st number 2nd number 3rd number - */ - return [ - (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, - ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, - ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000, - ]; -} - type EmojiMapping = [emoji: string, name: string]; const emojiMapping: EmojiMapping[] = [ diff --git a/src/crypto/verification/SASDecimal.ts b/src/crypto/verification/SASDecimal.ts new file mode 100644 index 00000000000..c8fa73100e6 --- /dev/null +++ b/src/crypto/verification/SASDecimal.ts @@ -0,0 +1,37 @@ +/* +Copyright 2018 - 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. +*/ + +/** + * Implementation of decimal encoding of SAS as per: + * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal + * @param sasBytes the five bytes generated by HKDF + * @returns the derived three numbers between 1000 and 9191 inclusive + */ +export function generateDecimalSas(sasBytes: number[]): [number, number, number] { + /** + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + return [ + (sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, + ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, + ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000, + ]; +} diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index 2347f467b51..92ad11a4cf1 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -50,7 +50,7 @@ export class MSC3906Rendezvous { public onFailure?: RendezvousFailureListener, ) {} - public get() { + public get code() { return this._code; } diff --git a/src/rendezvous/RendezvousChannel.ts b/src/rendezvous/RendezvousChannel.ts index 256016af921..4214d005d1d 100644 --- a/src/rendezvous/RendezvousChannel.ts +++ b/src/rendezvous/RendezvousChannel.ts @@ -32,13 +32,13 @@ export interface RendezvousChannel { * Send a payload via the channel. * @param data payload to send */ - send(data: any): Promise; + send(data: object): Promise; /** * Receive a payload from the channel. * @returns the received payload */ - receive(): Promise; + receive(): Promise; /** * Close the channel and clear up any resources. diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index d185b24cabb..1d6a473b5a9 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -41,15 +41,14 @@ export interface RendezvousTransport { /** * Send data via the transport. - * @param contentType the content type of the data being sent * @param data the data itself */ - send(contentType: string, data: any): Promise; + send(data: object): Promise; /** * Receive data from the transport. */ - receive(): Promise; + receive(): Promise; /** * Cancel the rendezvous. This will call `onCancelled()` if it is set. diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index f0a3f034921..c5ab9c2d028 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { SAS } from '@matrix-org/olm'; +import { TextEncoder } from 'util'; import { RendezvousError, @@ -28,7 +29,7 @@ import { } from '..'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; import { getCrypto } from '../../utils'; -import { generateDecimalSas } from '../../crypto/verification/SAS'; +import { generateDecimalSas } from '../../crypto/verification/SASDecimal'; const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? (window.crypto.subtle || window.crypto.webkitSubtle) : null; @@ -41,6 +42,13 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { }; } +interface ECDHPayload { + algorithm?: RendezvousChannelAlgorithm.ECDH_V1; + key?: string; + iv?: string; + ciphertext?: string; +} + async function importKey(key: Uint8Array): Promise { if (getCrypto()) { return key; @@ -108,7 +116,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { if (isInitiator) { // wait for the other side to send us their public key - const res = await this.receive(); + const res = await this.receive() as ECDHPayload | undefined; if (!res) { throw new Error('No response from other device'); } @@ -146,7 +154,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return generateDecimalSas(Array.from(rawChecksum)).join('-'); } - private async encrypt(data: any): Promise { + private async encrypt(data: string): Promise { if (this.aesKey instanceof Uint8Array) { const crypto = getCrypto(); @@ -158,10 +166,10 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { cipher.getAuthTag(), ]); - return JSON.stringify({ + return { iv: encodeBase64(iv), ciphertext: encodeBase64(ciphertext), - }); + }; } if (!subtleCrypto) { @@ -183,27 +191,23 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { encodedData, ); - return JSON.stringify({ + return { iv: encodeBase64(iv), ciphertext: encodeBase64(ciphertext), - }); + }; } - public async send(data: any) { + public async send(payload: object) { if (!this.olmSAS) { throw new Error('Channel closed'); } - const stringifiedData = JSON.stringify(data); + const data = this.aesKey ? await this.encrypt(JSON.stringify(payload)) : payload; - if (this.aesKey) { - await this.transport.send('application/json', await this.encrypt(stringifiedData)); - } else { - await this.transport.send('application/json', stringifiedData); - } + await this.transport.send(data); } - private async decrypt({ iv, ciphertext }: { iv: string, ciphertext: string }): Promise { + private async decrypt({ iv, ciphertext }: ECDHPayload): Promise { if (!ciphertext || !iv) { throw new Error('Missing ciphertext and/or iv'); } @@ -219,7 +223,9 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { "aes-256-gcm", this.aesKey as Uint8Array, decodeBase64(iv), { authTagLength: 16 }, ); decipher.setAuthTag(authTag); - return decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"); + return JSON.parse( + decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"), + ); } if (!subtleCrypto) { @@ -236,25 +242,24 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { ciphertextBytes, ); - return new TextDecoder().decode(new Uint8Array(plaintext)); + return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); } - public async receive(): Promise { + public async receive(): Promise { if (!this.olmSAS) { throw new Error('Channel closed'); } - const data = await this.transport.receive(); + const data = await this.transport.receive() as ECDHPayload | undefined; if (!data) { - return data; + return undefined; } if (data.ciphertext) { if (!this.aesKey) { throw new Error('Shared secret not set up'); } - const decrypted = await this.decrypt(data); - return JSON.parse(decrypted); + return this.decrypt(data); } else if (this.aesKey) { throw new Error('Data received but no ciphertext'); } diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index df63ad2c9b1..70ed28c9d52 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -100,7 +100,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport return this.fallbackRzServer; } - public async send(contentType: string, data: any) { + public async send(data: object) { if (this.cancelled) { return; } @@ -111,14 +111,14 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport throw new Error('Invalid rendezvous URI'); } - const headers: Record = { 'content-type': contentType }; + const headers: Record = { 'content-type': 'application/json' }; if (this.etag) { headers['if-match'] = this.etag; } const res = await this.fetch(uri, { method, headers, - body: data, + body: JSON.stringify(data), }); if (res.status === 404) { return this.cancel(RendezvousFailureReason.Unknown); @@ -143,7 +143,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - public async receive(): Promise { + public async receive(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } @@ -160,7 +160,8 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport const poll = await this.fetch(this.uri, { method: "GET", headers }); if (poll.status === 404) { - return this.cancel(RendezvousFailureReason.Unknown); + this.cancel(RendezvousFailureReason.Unknown); + return undefined; } // rely on server expiring the channel rather than checking ourselves @@ -169,9 +170,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport this.etag = poll.headers.get("etag") ?? undefined; } else if (poll.status === 200) { this.etag = poll.headers.get("etag") ?? undefined; - const data = await poll.json(); - - return data; + return poll.json(); } await sleep(1000); } From ceeecad00c2f75f69ba4068ff4cba33d3110f97e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:13:42 +0100 Subject: [PATCH 62/74] Rebase and remove Node.js crypto --- spec/unit/rendezvous/DummyTransport.ts | 2 +- spec/unit/rendezvous/ecdh.spec.ts | 28 ---------- .../MSC3903ECDHv1RendezvousChannel.ts | 53 +++---------------- 3 files changed, 8 insertions(+), 75 deletions(-) diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts index 3b0931624e6..35f89e206dc 100644 --- a/spec/unit/rendezvous/DummyTransport.ts +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -42,7 +42,7 @@ export class DummyTransport implements Ren async send(data: object): Promise { logger.info( `[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${ - data} where etag matches ${this.etag}`, + JSON.stringify(data)} where etag matches ${this.etag}`, ); // eslint-disable-next-line no-constant-condition while (!this.cancelled) { diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index fe7fd8e4261..4e94fff0f99 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -14,14 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import crypto from 'crypto'; - import '../../olm-loader'; import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; import { DummyTransport } from './DummyTransport'; -import { setCrypto } from '../../../src/utils'; describe('ECDHv1', function() { beforeAll(async function() { @@ -29,9 +26,6 @@ describe('ECDHv1', function() { }); describe('with crypto', () => { - beforeEach(async function() { - setCrypto(crypto); - }); it("initiator wants to sign in", async function() { const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); @@ -171,26 +165,4 @@ describe('ECDHv1', function() { await alice.cancel(RendezvousFailureReason.Unknown); }); }); - - it("no crypto", async function() { - // simulates running in a browser without crypto support - // n.b. we can't test subtle crypto because it's not available in jsdom jest environment - setCrypto(undefined as unknown as typeof crypto); - - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - - expect(bob.connect()).rejects.toThrowError(); - expect(alice.connect()).rejects.toThrowError(); - - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); - }); }); diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index c5ab9c2d028..0fd16948929 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { SAS } from '@matrix-org/olm'; -import { TextEncoder } from 'util'; import { RendezvousError, @@ -28,12 +27,9 @@ import { RendezvousChannelAlgorithm, } from '..'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; -import { getCrypto } from '../../utils'; +import { crypto, subtleCrypto, TextEncoder } from '../../crypto/crypto'; import { generateDecimalSas } from '../../crypto/verification/SASDecimal'; -const subtleCrypto = (typeof window !== "undefined" && window.crypto) ? - (window.crypto.subtle || window.crypto.webkitSubtle) : null; - export interface ECDHv1RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; @@ -49,13 +45,9 @@ interface ECDHPayload { ciphertext?: string; } -async function importKey(key: Uint8Array): Promise { - if (getCrypto()) { - return key; - } - +async function importKey(key: Uint8Array): Promise { if (!subtleCrypto) { - throw new Error('Neither Web Crypto nor Node.js crypto are available'); + throw new Error('Web Crypto is not available'); } const imported = subtleCrypto.importKey( @@ -77,7 +69,7 @@ async function importKey(key: Uint8Array): Promise { export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; - private aesKey?: CryptoKey | Uint8Array; + private aesKey?: CryptoKey; public constructor( public transport: RendezvousTransport, @@ -155,29 +147,12 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } private async encrypt(data: string): Promise { - if (this.aesKey instanceof Uint8Array) { - const crypto = getCrypto(); - - const iv = crypto.randomBytes(32); - const cipher = crypto.createCipheriv("aes-256-gcm", this.aesKey as Uint8Array, iv, { authTagLength: 16 }); - const ciphertext = Buffer.concat([ - cipher.update(data, "utf8"), - cipher.final(), - cipher.getAuthTag(), - ]); - - return { - iv: encodeBase64(iv), - ciphertext: encodeBase64(ciphertext), - }; - } - if (!subtleCrypto) { - throw new Error('Neither Web Crypto nor Node.js crypto are available'); + throw new Error('Web Crypto is not available'); } const iv = new Uint8Array(32); - window.crypto.getRandomValues(iv); + crypto.getRandomValues(iv); const encodedData = new TextEncoder().encode(data); @@ -214,22 +189,8 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const ciphertextBytes = decodeBase64(ciphertext); - if (this.aesKey instanceof Uint8Array) { - const crypto = getCrypto(); - // in contrast to Web Crypto API, Node's crypto needs the auth tag split off the cipher text - const ciphertextOnly = ciphertextBytes.slice(0, ciphertextBytes.length - 16); - const authTag = ciphertextBytes.slice(ciphertextBytes.length - 16); - const decipher = crypto.createDecipheriv( - "aes-256-gcm", this.aesKey as Uint8Array, decodeBase64(iv), { authTagLength: 16 }, - ); - decipher.setAuthTag(authTag); - return JSON.parse( - decipher.update(encodeBase64(ciphertextOnly), "base64", "utf-8") + decipher.final("utf-8"), - ); - } - if (!subtleCrypto) { - throw new Error('Neither Web Crypto nor Node.js crypto are available'); + throw new Error('Web Crypto is not available'); } const plaintext = await subtleCrypto.decrypt( From 1f6838af18b875dbb57a73a34e237f77fd6b157b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:15:36 +0100 Subject: [PATCH 63/74] Wipe AES key out after use --- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 0fd16948929..0285eabdddd 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -142,6 +142,9 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { 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('-'); } From d8bb398f6a4110ad2c13bd4302cf059de12912af Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:28:09 +0100 Subject: [PATCH 64/74] Add typing for MSC3906 layer --- src/rendezvous/MSC3906Rendezvous.ts | 54 ++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index 92ad11a4cf1..0ea13abb66d 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -31,6 +31,29 @@ enum PayloadType { Progress = 'm.login.progress', } +enum Outcome { + Success = 'success', + Failure = 'failure', + Verified = 'verified', + Declined = 'declined', + Unsupported = 'unsupported', +} + +interface RendezvousPayload { + 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"); /** @@ -44,16 +67,27 @@ export class MSC3906Rendezvous { 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, ) {} - public get code() { + /** + * Returns the code representing the rendezvous suitable for rendering in a QR code. + */ + public get code(): string { return this._code; } + /** + * Generate the code including doing partial set up of the channel where required. + */ public async generateCode(): Promise { if (this._code) { return; @@ -71,7 +105,7 @@ export class MSC3906Rendezvous { // determine available protocols if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { logger.info("Server doesn't support MSC3882"); - await this.send({ type: PayloadType.Finish, outcome: 'unsupported' }); + await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported }); await this.cancel(RendezvousFailureReason.HomeserverLacksSupport); return undefined; } @@ -79,7 +113,7 @@ export class MSC3906Rendezvous { 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.channel.receive(); + const { type, protocol, outcome } = await this.receive(); if (type === PayloadType.Finish) { // new device decided not to complete @@ -106,13 +140,17 @@ export class MSC3906Rendezvous { return checksum; } - private async send({ type, ...payload }: { type: PayloadType, [key: string]: any }) { - await this.channel.send({ type, ...payload }); + private async receive(): Promise { + return await this.channel.receive() as RendezvousPayload; + } + + private async send(payload: RendezvousPayload) { + await this.channel.send(payload); } public async declineLoginOnExistingDevice() { logger.info('User declined sign in'); - await this.send({ type: PayloadType.Finish, outcome: 'declined' }); + await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined }); } public async approveLoginOnExistingDevice(loginToken: string): Promise { @@ -120,7 +158,7 @@ export class MSC3906Rendezvous { await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); logger.info('Waiting for outcome'); - const res = await this.channel.receive(); + const res = await this.receive(); if (!res) { return undefined; } @@ -169,7 +207,7 @@ export class MSC3906Rendezvous { await this.send({ type: PayloadType.Finish, - outcome: 'verified', + outcome: Outcome.Verified, verifying_device_id: this.client.getDeviceId(), verifying_device_key: this.client.getDeviceEd25519Key(), master_key: masterPublicKey, From 3c33b452c2d0195122b4adb8463b847dcf48f002 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 17 Oct 2022 22:34:32 +0100 Subject: [PATCH 65/74] Strict lint --- spec/unit/rendezvous/DummyTransport.ts | 4 ++-- src/rendezvous/MSC3906Rendezvous.ts | 6 +++--- src/rendezvous/RendezvousChannel.ts | 2 +- src/rendezvous/RendezvousTransport.ts | 2 +- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 2 +- .../transports/MSC3886SimpleHttpRendezvousTransport.ts | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts index 35f89e206dc..d40fd2633f6 100644 --- a/spec/unit/rendezvous/DummyTransport.ts +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -27,7 +27,7 @@ export class DummyTransport implements Ren otherParty?: DummyTransport; etag?: string; lastEtagReceived?: string; - data: object | null = null; + data: object = {}; ready = false; cancelled = false; @@ -77,7 +77,7 @@ export class DummyTransport implements Ren await sleep(250); } - return undefined; + return {}; } cancel(reason: RendezvousFailureReason): Promise { diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index 0ea13abb66d..a0b5cc81cf2 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -79,9 +79,9 @@ export class MSC3906Rendezvous { ) {} /** - * Returns the code representing the rendezvous suitable for rendering in a QR code. + * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. */ - public get code(): string { + public get code(): string | undefined { return this._code; } @@ -132,7 +132,7 @@ export class MSC3906Rendezvous { return undefined; } - if (!LOGIN_TOKEN_PROTOCOL.matches(protocol)) { + if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) { await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm); return undefined; } diff --git a/src/rendezvous/RendezvousChannel.ts b/src/rendezvous/RendezvousChannel.ts index 4214d005d1d..2e098f763c2 100644 --- a/src/rendezvous/RendezvousChannel.ts +++ b/src/rendezvous/RendezvousChannel.ts @@ -38,7 +38,7 @@ export interface RendezvousChannel { * Receive a payload from the channel. * @returns the received payload */ - receive(): Promise; + receive(): Promise; /** * Close the channel and clear up any resources. diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index 1d6a473b5a9..5c6aa3d15e6 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -48,7 +48,7 @@ export interface RendezvousTransport { /** * Receive data from the transport. */ - receive(): Promise; + receive(): Promise; /** * Cancel the rendezvous. This will call `onCancelled()` if it is set. diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 0285eabdddd..5f30a3e5480 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -209,7 +209,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); } - public async receive(): Promise { + public async receive(): Promise { if (!this.olmSAS) { throw new Error('Channel closed'); } diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index 70ed28c9d52..b98eb34b49d 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -143,14 +143,14 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - public async receive(): Promise { + public async receive(): Promise { if (!this.uri) { throw new Error('Rendezvous not set up'); } // eslint-disable-next-line no-constant-condition while (true) { if (this.cancelled) { - return; + return undefined; } const headers: Record = {}; From 3319ca4bc3c1b4bbcd1528e260fd59115f8efa17 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 09:49:26 +0100 Subject: [PATCH 66/74] Fix double connect detection --- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 5f30a3e5480..57368ae028e 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -70,6 +70,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey; + private connected = false; public constructor( public transport: RendezvousTransport, @@ -100,6 +101,10 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } public async connect(): Promise { + if (this.connected) { + throw new Error('Channel already connected'); + } + if (!this.olmSAS) { throw new Error('Channel closed'); } @@ -130,6 +135,8 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { }); } + this.connected = true; + this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!)); const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; @@ -176,6 +183,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } public async send(payload: object) { + console.log(JSON.stringify(payload)); if (!this.olmSAS) { throw new Error('Channel closed'); } From 3cd46cd33ffb54d8b071abfc15e9e807c94490d8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 09:51:25 +0100 Subject: [PATCH 67/74] Remove unintended debug statement --- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 57368ae028e..bc1302272ca 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -183,7 +183,6 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } public async send(payload: object) { - console.log(JSON.stringify(payload)); if (!this.olmSAS) { throw new Error('Channel closed'); } From f8a130bd986a00b34ce7b33e1b4c9ede64d2a56c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 15:19:28 +0100 Subject: [PATCH 68/74] Return types --- src/rendezvous/MSC3906Rendezvous.ts | 6 +++--- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 6 +++--- .../transports/MSC3886SimpleHttpRendezvousTransport.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index a0b5cc81cf2..cfb92abe04a 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -174,7 +174,7 @@ export class MSC3906Rendezvous { return deviceId; } - private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo) { + private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo): Promise { if (!this.client.crypto) { throw new Error('Crypto not available on client'); } @@ -258,12 +258,12 @@ export class MSC3906Rendezvous { throw new Error('Device not online within timeout'); } - public async cancel(reason: RendezvousFailureReason) { + public async cancel(reason: RendezvousFailureReason): Promise { this.onFailure?.(reason); await this.channel.cancel(reason); } - public async close() { + public async close(): Promise { await this.channel.close(); } } diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index bc1302272ca..1c2fb47f31b 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -182,7 +182,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { }; } - public async send(payload: object) { + public async send(payload: object): Promise { if (!this.olmSAS) { throw new Error('Channel closed'); } @@ -238,14 +238,14 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return data; } - public async close() { + public async close(): Promise { if (this.olmSAS) { this.olmSAS.free(); this.olmSAS = undefined; } } - public async cancel(reason: RendezvousFailureReason) { + public async cancel(reason: RendezvousFailureReason): Promise { try { await this.transport.cancel(reason); } finally { diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index b98eb34b49d..52a1812a137 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -66,7 +66,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport this.fallbackRzServer = fallbackRzServer; } - get ready() { + public get ready(): boolean { return this._ready; } @@ -100,7 +100,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport return this.fallbackRzServer; } - public async send(data: object) { + public async send(data: object): Promise { if (this.cancelled) { return; } @@ -176,7 +176,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - public async cancel(reason: RendezvousFailureReason) { + public async cancel(reason: RendezvousFailureReason): Promise { if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) { reason = RendezvousFailureReason.Expired; From c6291a7319a7c0bebd0afb28ddc9e139250de1d6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:29:27 +0100 Subject: [PATCH 69/74] Use generics --- spec/unit/rendezvous/DummyTransport.ts | 14 +++--- spec/unit/rendezvous/ecdh.spec.ts | 30 +++++++------ spec/unit/rendezvous/rendezvous.spec.ts | 43 ++++++++++++------- src/rendezvous/MSC3906Rendezvous.ts | 10 ++--- src/rendezvous/RendezvousChannel.ts | 8 ++-- src/rendezvous/RendezvousTransport.ts | 6 +-- .../MSC3903ECDHv1RendezvousChannel.ts | 40 ++++++++--------- .../MSC3886SimpleHttpRendezvousTransport.ts | 6 +-- 8 files changed, 86 insertions(+), 71 deletions(-) diff --git a/spec/unit/rendezvous/DummyTransport.ts b/spec/unit/rendezvous/DummyTransport.ts index d40fd2633f6..617b0380c71 100644 --- a/spec/unit/rendezvous/DummyTransport.ts +++ b/spec/unit/rendezvous/DummyTransport.ts @@ -23,23 +23,23 @@ import { } from "../../../src/rendezvous"; import { sleep } from '../../../src/utils'; -export class DummyTransport implements RendezvousTransport { - otherParty?: DummyTransport; +export class DummyTransport implements RendezvousTransport { + otherParty?: DummyTransport; etag?: string; lastEtagReceived?: string; - data: object = {}; + data: T | undefined; ready = false; cancelled = false; - constructor(private name: string, private mockDetails: T) {} + constructor(private name: string, private mockDetails: D) {} onCancelled?: RendezvousFailureListener; details(): Promise { return Promise.resolve(this.mockDetails); } - async send(data: object): Promise { + async send(data: T): Promise { logger.info( `[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${ JSON.stringify(data)} where etag matches ${this.etag}`, @@ -60,7 +60,7 @@ export class DummyTransport implements Ren } } - async receive(): Promise { + async receive(): Promise { logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`); // eslint-disable-next-line no-constant-condition while (!this.cancelled) { @@ -77,7 +77,7 @@ export class DummyTransport implements Ren await sleep(250); } - return {}; + return undefined; } cancel(reason: RendezvousFailureReason): Promise { diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 4e94fff0f99..1b69e5a0c54 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -16,10 +16,14 @@ limitations under the License. import '../../olm-loader'; import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; +import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels'; import { decodeBase64 } from '../../../src/crypto/olmlib'; import { DummyTransport } from './DummyTransport'; +function makeTransport(name: string) { + return new DummyTransport(name, { type: 'dummy' }); +} + describe('ECDHv1', function() { beforeAll(async function() { await global.Olm.init(); @@ -27,8 +31,8 @@ describe('ECDHv1', function() { describe('with crypto', () => { it("initiator wants to sign in", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -52,8 +56,8 @@ describe('ECDHv1', function() { }); it("initiator wants to reciprocate", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -77,8 +81,8 @@ describe('ECDHv1', function() { }); it("double connect", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -99,8 +103,8 @@ describe('ECDHv1', function() { }); it("closed", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -125,8 +129,8 @@ describe('ECDHv1', function() { }); it("require ciphertext", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -149,8 +153,8 @@ describe('ECDHv1', function() { }); it("ciphertext before set up", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'dummy' }); - const bobTransport = new DummyTransport('Bob', { type: 'dummy' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob'); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 476a8d050ec..2e0f492c046 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -17,8 +17,17 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; import '../../olm-loader'; -import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { ECDHv1RendezvousCode, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels"; +import { + MSC3906Rendezvous, + RendezvousCode, + RendezvousFailureReason, + RendezvousIntent, +} from "../../../src/rendezvous"; +import { + ECDHv1RendezvousCode, + MSC3903ECDHPayload, + MSC3903ECDHv1RendezvousChannel, +} from "../../../src/rendezvous/channels"; import { MatrixClient } from "../../../src"; import { MSC3886SimpleHttpRendezvousTransport, @@ -68,6 +77,10 @@ function makeMockClient(opts: { } as unknown as MatrixClient; } +function makeTransport(name: string, uri = 'https://test.rz/123456') { + return new DummyTransport(name, { type: 'http.v1', uri }); +} + describe("Rendezvous", function() { beforeAll(async function() { await global.Olm.init(); @@ -75,7 +88,7 @@ describe("Rendezvous", function() { let httpBackend: MockHttpBackend; let fetchFn: typeof global.fetchFn; - let transports: DummyTransport[]; + let transports: DummyTransport[]; beforeEach(function() { httpBackend = new MockHttpBackend(); @@ -148,8 +161,8 @@ describe("Rendezvous", function() { }); it("no protocols", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -202,8 +215,8 @@ describe("Rendezvous", function() { }); it("new device declines protocol", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice', 'https://test.rz/123456'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -260,8 +273,8 @@ describe("Rendezvous", function() { }); it("new device declines protocol", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice', 'https://test.rz/123456'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -318,8 +331,8 @@ describe("Rendezvous", function() { }); it("decline on existing device", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice', 'https://test.rz/123456'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -378,8 +391,8 @@ describe("Rendezvous", function() { }); it("approve on existing device + no verification", async function() { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice', 'https://test.rz/123456'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; @@ -445,8 +458,8 @@ describe("Rendezvous", function() { }); async function completeLogin(devices: Record>) { - const aliceTransport = new DummyTransport('Alice', { type: 'http.v1', uri: 'https://test.rz/123456' }); - const bobTransport = new DummyTransport('Bob', { type: 'http.v1', uri: 'https://test.rz/999999' }); + const aliceTransport = makeTransport('Alice', 'https://test.rz/123456'); + const bobTransport = makeTransport('Bob', 'https://test.rz/999999'); transports.push(aliceTransport, bobTransport); aliceTransport.otherParty = bobTransport; bobTransport.otherParty = aliceTransport; diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index cfb92abe04a..a24216751df 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -39,7 +39,7 @@ enum Outcome { Unsupported = 'unsupported', } -interface RendezvousPayload { +export interface MSC3906RendezvousPayload { type: PayloadType; intent?: RendezvousIntent; outcome?: Outcome; @@ -73,7 +73,7 @@ export class MSC3906Rendezvous { * @param onFailure Callback for when the rendezvous fails */ public constructor( - private channel: RendezvousChannel, + private channel: RendezvousChannel, private client: MatrixClient, public onFailure?: RendezvousFailureListener, ) {} @@ -140,11 +140,11 @@ export class MSC3906Rendezvous { return checksum; } - private async receive(): Promise { - return await this.channel.receive() as RendezvousPayload; + private async receive(): Promise { + return await this.channel.receive() as MSC3906RendezvousPayload; } - private async send(payload: RendezvousPayload) { + private async send(payload: MSC3906RendezvousPayload) { await this.channel.send(payload); } diff --git a/src/rendezvous/RendezvousChannel.ts b/src/rendezvous/RendezvousChannel.ts index 2e098f763c2..9b1060304c9 100644 --- a/src/rendezvous/RendezvousChannel.ts +++ b/src/rendezvous/RendezvousChannel.ts @@ -16,13 +16,11 @@ limitations under the License. import { RendezvousCode, - RendezvousTransport, RendezvousIntent, RendezvousFailureReason, } from "."; -export interface RendezvousChannel { - transport: RendezvousTransport; +export interface RendezvousChannel { /** * @returns the checksum/confirmation digits to be shown to the user */ @@ -32,13 +30,13 @@ export interface RendezvousChannel { * Send a payload via the channel. * @param data payload to send */ - send(data: object): Promise; + send(data: T): Promise; /** * Receive a payload from the channel. * @returns the received payload */ - receive(): Promise; + receive(): Promise | undefined>; /** * Close the channel and clear up any resources. diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index 5c6aa3d15e6..231a0f1c48a 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -23,7 +23,7 @@ export interface RendezvousTransportDetails { /** * Interface representing a generic rendezvous transport. */ -export interface RendezvousTransport { +export interface RendezvousTransport { /** * Ready state of the transport. This is set to true when the transport is ready to be used. */ @@ -43,12 +43,12 @@ export interface RendezvousTransport { * Send data via the transport. * @param data the data itself */ - send(data: object): Promise; + send(data: T): Promise; /** * Receive data from the transport. */ - receive(): Promise; + receive(): Promise | undefined>; /** * Cancel the rendezvous. This will call `onCancelled()` if it is set. diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 1c2fb47f31b..c9ee137a329 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -38,7 +38,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { }; } -interface ECDHPayload { +export interface MSC3903ECDHPayload { algorithm?: RendezvousChannelAlgorithm.ECDH_V1; key?: string; iv?: string; @@ -66,14 +66,14 @@ async function importKey(key: Uint8Array): Promise { * X25519/ECDH key agreement based secure rendezvous channel. * Note that this is UNSTABLE and may have breaking changes without notice. */ -export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { +export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { private olmSAS?: SAS; private ourPublicKey: Uint8Array; private aesKey?: CryptoKey; private connected = false; public constructor( - public transport: RendezvousTransport, + private transport: RendezvousTransport, private theirPublicKey?: Uint8Array, public onFailure?: (reason: RendezvousFailureReason) => void, ) { @@ -86,7 +86,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Code already generated'); } - await this.send({ algorithm: RendezvousChannelAlgorithm.ECDH_V1 }); + await this.transport.send({ algorithm: RendezvousChannelAlgorithm.ECDH_V1 }); const rendezvous: ECDHv1RendezvousCode = { "rendezvous": { @@ -113,7 +113,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { if (isInitiator) { // wait for the other side to send us their public key - const res = await this.receive() as ECDHPayload | undefined; + const res = await this.transport.receive(); if (!res) { throw new Error('No response from other device'); } @@ -129,7 +129,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { this.theirPublicKey = decodeBase64(key); } else { // send our public key unencrypted - await this.send({ + await this.transport.send({ algorithm: RendezvousChannelAlgorithm.ECDH_V1, key: encodeBase64(this.ourPublicKey), }); @@ -156,7 +156,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return generateDecimalSas(Array.from(rawChecksum)).join('-'); } - private async encrypt(data: string): Promise { + private async encrypt(data: T): Promise { if (!subtleCrypto) { throw new Error('Web Crypto is not available'); } @@ -164,7 +164,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const iv = new Uint8Array(32); crypto.getRandomValues(iv); - const encodedData = new TextEncoder().encode(data); + const encodedData = new TextEncoder().encode(JSON.stringify(data)); const ciphertext = await subtleCrypto.encrypt( { @@ -182,17 +182,19 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { }; } - public async send(payload: object): Promise { + public async send(payload: T): Promise { if (!this.olmSAS) { throw new Error('Channel closed'); } - const data = this.aesKey ? await this.encrypt(JSON.stringify(payload)) : payload; + if (!this.aesKey) { + throw new Error('Shared secret not set up'); + } - await this.transport.send(data); + return this.transport.send((await this.encrypt(payload))); } - private async decrypt({ iv, ciphertext }: ECDHPayload): Promise { + private async decrypt({ iv, ciphertext }: MSC3903ECDHPayload): Promise> { if (!ciphertext || !iv) { throw new Error('Missing ciphertext and/or iv'); } @@ -216,26 +218,24 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); } - public async receive(): Promise { + 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 data = await this.transport.receive() as ECDHPayload | undefined; + const data = await this.transport.receive(); if (!data) { return undefined; } if (data.ciphertext) { - if (!this.aesKey) { - throw new Error('Shared secret not set up'); - } return this.decrypt(data); - } else if (this.aesKey) { - throw new Error('Data received but no ciphertext'); } - return data; + throw new Error('Data received but no ciphertext'); } public async close(): Promise { diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts index 52a1812a137..cffbefe5a26 100644 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts @@ -38,7 +38,7 @@ export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousT * simple HTTP rendezvous protocol. * Note that this is UNSTABLE and may have breaking changes without notice. */ -export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { +export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport { private uri?: string; private etag?: string; private expiresAt?: Date; @@ -100,7 +100,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport return this.fallbackRzServer; } - public async send(data: object): Promise { + public async send(data: T): Promise { if (this.cancelled) { return; } @@ -143,7 +143,7 @@ export class MSC3886SimpleHttpRendezvousTransport implements RendezvousTransport } } - public async receive(): Promise { + public async receive(): Promise | undefined> { if (!this.uri) { throw new Error('Rendezvous not set up'); } From 710401d9dda47fb3fcc9fba9fcd76f7f19439475 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:37:05 +0100 Subject: [PATCH 70/74] Make type of MSC3903ECDHPayload explicit --- .../MSC3903ECDHv1RendezvousChannel.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index c9ee137a329..ea27f33285d 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -38,11 +38,16 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { }; } -export interface MSC3903ECDHPayload { - algorithm?: RendezvousChannelAlgorithm.ECDH_V1; +export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; + +export interface PlainTextPayload { + algorithm: RendezvousChannelAlgorithm.ECDH_V1; key?: string; - iv?: string; - ciphertext?: string; +} + +export interface EncryptedPayload { + iv: string; + ciphertext: string; } async function importKey(key: Uint8Array): Promise { @@ -113,12 +118,12 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { if (isInitiator) { // wait for the other side to send us their public key - const res = await this.transport.receive(); - if (!res) { + 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 !== RendezvousChannelAlgorithm.ECDH_V1 || !key) { throw new RendezvousError( 'Unsupported algorithm: ' + algorithm, @@ -194,7 +199,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { return this.transport.send((await this.encrypt(payload))); } - private async decrypt({ iv, ciphertext }: MSC3903ECDHPayload): Promise> { + private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise> { if (!ciphertext || !iv) { throw new Error('Missing ciphertext and/or iv'); } @@ -226,13 +231,13 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Shared secret not set up'); } - const data = await this.transport.receive(); - if (!data) { + const rawData = await this.transport.receive(); + if (!rawData) { return undefined; } - - if (data.ciphertext) { - return this.decrypt(data); + const data = rawData as Partial; + if (data.ciphertext && data.iv) { + return this.decrypt(data as EncryptedPayload); } throw new Error('Data received but no ciphertext'); From 87fca05f5b6645e3f06b46e9e5fcba2b26c7d4ad Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:58:27 +0100 Subject: [PATCH 71/74] Use unstable prefix for RendezvousChannelAlgorithm --- src/rendezvous/RendezvousChannelAlgorithm.ts | 19 ----------------- .../MSC3903ECDHv1RendezvousChannel.ts | 21 ++++++++++++------- src/rendezvous/index.ts | 1 - 3 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 src/rendezvous/RendezvousChannelAlgorithm.ts diff --git a/src/rendezvous/RendezvousChannelAlgorithm.ts b/src/rendezvous/RendezvousChannelAlgorithm.ts deleted file mode 100644 index 7022cd5c874..00000000000 --- a/src/rendezvous/RendezvousChannelAlgorithm.ts +++ /dev/null @@ -1,19 +0,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. -*/ - -export enum RendezvousChannelAlgorithm { - ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" -} diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index ea27f33285d..0560c4b81f8 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -24,16 +24,21 @@ import { RendezvousTransportDetails, RendezvousTransport, RendezvousFailureReason, - RendezvousChannelAlgorithm, } from '..'; import { encodeBase64, decodeBase64 } from '../../crypto/olmlib'; import { crypto, subtleCrypto, TextEncoder } from '../../crypto/crypto'; import { generateDecimalSas } from '../../crypto/verification/SASDecimal'; +import { UnstableValue } from '../../NamespacedValue'; + +const ECDH_V1 = new UnstableValue( + "m.rendezvous.v1.curve25519-aes-sha256", + "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256", +); export interface ECDHv1RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: RendezvousChannelAlgorithm.ECDH_V1; + algorithm: typeof ECDH_V1.name; key: string; }; } @@ -41,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; export interface PlainTextPayload { - algorithm: RendezvousChannelAlgorithm.ECDH_V1; + algorithm: typeof ECDH_V1.name; key?: string; } @@ -91,11 +96,11 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { throw new Error('Code already generated'); } - await this.transport.send({ algorithm: RendezvousChannelAlgorithm.ECDH_V1 }); + await this.transport.send({ algorithm: ECDH_V1.name }); const rendezvous: ECDHv1RendezvousCode = { "rendezvous": { - algorithm: RendezvousChannelAlgorithm.ECDH_V1, + algorithm: ECDH_V1.name, key: encodeBase64(this.ourPublicKey), transport: await this.transport.details(), }, @@ -124,7 +129,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } const res = rawRes as Partial; const { key, algorithm } = res; - if (algorithm !== RendezvousChannelAlgorithm.ECDH_V1 || !key) { + if (!algorithm || !ECDH_V1.matches(algorithm) || !key) { throw new RendezvousError( 'Unsupported algorithm: ' + algorithm, RendezvousFailureReason.UnsupportedAlgorithm, @@ -135,7 +140,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { } else { // send our public key unencrypted await this.transport.send({ - algorithm: RendezvousChannelAlgorithm.ECDH_V1, + algorithm: ECDH_V1.name, key: encodeBase64(this.ourPublicKey), }); } @@ -146,7 +151,7 @@ export class MSC3903ECDHv1RendezvousChannel implements RendezvousChannel { const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = RendezvousChannelAlgorithm.ECDH_V1.toString(); + let aesInfo = ECDH_V1.name; aesInfo += `|${encodeBase64(initiatorKey)}`; aesInfo += `|${encodeBase64(recipientKey)}`; diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index a5e57155e30..7e506b45052 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -16,7 +16,6 @@ limitations under the License. export * from './MSC3906Rendezvous'; export * from './RendezvousChannel'; -export * from './RendezvousChannelAlgorithm'; export * from './RendezvousCode'; export * from './RendezvousError'; export * from './RendezvousFailureReason'; From 1dff0f4d315f69628a011aaa7461886d53b28e0e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 16:59:56 +0100 Subject: [PATCH 72/74] Fix --- src/rendezvous/RendezvousCode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendezvous/RendezvousCode.ts b/src/rendezvous/RendezvousCode.ts index 59e7d0744b9..86608aa1c44 100644 --- a/src/rendezvous/RendezvousCode.ts +++ b/src/rendezvous/RendezvousCode.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { RendezvousChannelAlgorithm, RendezvousTransportDetails, RendezvousIntent } from "."; +import { RendezvousTransportDetails, RendezvousIntent } from "."; export interface RendezvousCode { intent: RendezvousIntent; rendezvous?: { transport: RendezvousTransportDetails; - algorithm: RendezvousChannelAlgorithm; + algorithm: string; }; } From 4e0ff75529a0037d0740fe8e126000e05f5565f2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 17:05:07 +0100 Subject: [PATCH 73/74] Extra unstable type --- src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts index 0560c4b81f8..d3d5fc6807b 100644 --- a/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts +++ b/src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts @@ -38,7 +38,7 @@ const ECDH_V1 = new UnstableValue( export interface ECDHv1RendezvousCode extends RendezvousCode { rendezvous: { transport: RendezvousTransportDetails; - algorithm: typeof ECDH_V1.name; + algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; key: string; }; } @@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode { export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload; export interface PlainTextPayload { - algorithm: typeof ECDH_V1.name; + algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName; key?: string; } From a067213463b1479af93fb1f54bc6d869a5f6d6ef Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Oct 2022 17:08:02 +0100 Subject: [PATCH 74/74] Test types --- spec/unit/rendezvous/ecdh.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/rendezvous/ecdh.spec.ts b/spec/unit/rendezvous/ecdh.spec.ts index 1b69e5a0c54..b8f40b52c3b 100644 --- a/spec/unit/rendezvous/ecdh.spec.ts +++ b/spec/unit/rendezvous/ecdh.spec.ts @@ -145,7 +145,7 @@ describe('ECDHv1', function() { expect(aliceChecksum).toEqual(bobChecksum); // send a message without encryption - await aliceTransport.send({}); + await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" }); expect(bob.receive()).rejects.toThrowError(); await alice.cancel(RendezvousFailureReason.Unknown); @@ -162,7 +162,7 @@ describe('ECDHv1', function() { const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport); await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - await bobTransport.send({ ciphertext: "foo" }); + await bobTransport.send({ iv: "dummy", ciphertext: "dummy" }); expect(alice.receive()).rejects.toThrowError();