1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

Remove abandoned MSC3886, MSC3903, MSC3906 implementations (#4469)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2024-10-24 14:19:29 +01:00
committed by GitHub
parent 8b8ee91210
commit 23c4c9fd8a
10 changed files with 1 additions and 2095 deletions

View File

@@ -1,276 +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.
*/
import { UnstableValue } from "matrix-events-sdk";
import {
RendezvousChannel,
RendezvousFailureListener,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousIntent,
} from "./index.ts";
import { MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client.ts";
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature.ts";
import { logger } from "../logger.ts";
import { sleep } from "../utils.ts";
import { CrossSigningKey } from "../crypto-api/index.ts";
import { Capabilities, Device, IGetLoginTokenCapability } from "../matrix.ts";
enum PayloadType {
Start = "m.login.start",
Finish = "m.login.finish",
Progress = "m.login.progress",
}
enum Outcome {
Success = "success",
Failure = "failure",
Verified = "verified",
Declined = "declined",
Unsupported = "unsupported",
}
export interface MSC3906RendezvousPayload {
type: PayloadType;
intent?: RendezvousIntent;
outcome?: Outcome;
device_id?: string;
device_key?: string;
verifying_device_id?: string;
verifying_device_key?: string;
master_key?: string;
protocols?: string[];
protocol?: string;
login_token?: string;
homeserver?: string;
}
const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc3906.login_token");
/**
* Implements MSC3906 to allow a user to sign in on a new device using QR code.
* This implementation only supports generating a QR code on a device that is already signed in.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3906Rendezvous {
private newDeviceId?: string;
private newDeviceKey?: string;
private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
private _code?: string;
/**
* @param channel - The secure channel used for communication
* @param client - The Matrix client in used on the device already logged in
* @param onFailure - Callback for when the rendezvous fails
*/
public constructor(
private channel: RendezvousChannel<MSC3906RendezvousPayload>,
private client: MatrixClient,
public onFailure?: RendezvousFailureListener,
) {}
/**
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
*/
public get code(): string | undefined {
return this._code;
}
/**
* Generate the code including doing partial set up of the channel where required.
*/
public async generateCode(): Promise<void> {
if (this._code) {
return;
}
this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent));
}
public async startAfterShowingCode(): Promise<string | undefined> {
const checksum = await this.channel.connect();
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
// in stable and unstable r1 the availability is exposed as a capability
let capabilities: Capabilities = {};
try {
capabilities = await this.client.getCapabilities();
} catch {}
// in r0 of MSC3882 the availability is exposed as a feature flag
const features = await buildFeatureSupportMap(await this.client.getVersions());
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
// determine available protocols
if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
logger.info("Server doesn't support get_login_token");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
return undefined;
}
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
logger.info("Waiting for other device to choose protocol");
const { type, protocol, outcome } = await this.receive();
if (type === PayloadType.Finish) {
// new device decided not to complete
switch (outcome ?? "") {
case "unsupported":
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
break;
default:
await this.cancel(RendezvousFailureReason.Unknown);
}
return undefined;
}
if (type !== PayloadType.Progress) {
await this.cancel(RendezvousFailureReason.Unknown);
return undefined;
}
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) {
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
return undefined;
}
return checksum;
}
private async receive(): Promise<MSC3906RendezvousPayload> {
return (await this.channel.receive()) as MSC3906RendezvousPayload;
}
private async send(payload: MSC3906RendezvousPayload): Promise<void> {
await this.channel.send(payload);
}
public async declineLoginOnExistingDevice(): Promise<void> {
logger.info("User declined sign in");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
}
public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> {
// eslint-disable-next-line camelcase
await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl });
logger.info("Waiting for outcome");
const res = await this.receive();
if (!res) {
return undefined;
}
const { outcome, device_id: deviceId, device_key: deviceKey } = res;
if (outcome !== "success") {
throw new Error("Linking failed");
}
this.newDeviceId = deviceId;
this.newDeviceKey = deviceKey;
return deviceId;
}
private async verifyAndCrossSignDevice(deviceInfo: Device): Promise<void> {
const crypto = this.client.getCrypto()!;
if (!this.newDeviceId) {
throw new Error("No new device ID set");
}
// check that keys received from the server for the new device match those received from the device itself
if (deviceInfo.getFingerprint() !== this.newDeviceKey) {
throw new Error(
`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`,
);
}
const userId = this.client.getSafeUserId();
// mark the device as verified locally + cross sign
logger.info(`Marking device ${this.newDeviceId} as verified`);
await crypto.setDeviceVerified(userId, this.newDeviceId, true);
await crypto.crossSignDevice(this.newDeviceId);
const masterPublicKey = (await crypto.getCrossSigningKeyId(CrossSigningKey.Master)) ?? undefined;
const ourDeviceId = this.client.getDeviceId()!;
const ourDeviceKey = (await crypto.getOwnDeviceKeys()).ed25519;
await this.send({
type: PayloadType.Finish,
outcome: Outcome.Verified,
verifying_device_id: ourDeviceId,
verifying_device_key: ourDeviceKey,
master_key: masterPublicKey,
});
}
/**
* Verify the device and cross-sign it.
* @param timeout - time in milliseconds to wait for device to come online
*/
public async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise<void> {
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 crypto = this.client.getCrypto();
if (!crypto) {
throw new Error("Crypto not available on client");
}
let deviceInfo = await this.getOwnDevice(this.newDeviceId);
if (!deviceInfo) {
logger.info("Going to wait for new device to be online");
await sleep(timeout);
deviceInfo = await this.getOwnDevice(this.newDeviceId);
}
if (deviceInfo) {
await this.verifyAndCrossSignDevice(deviceInfo);
return;
}
throw new Error("Device not online within timeout");
}
private async getOwnDevice(deviceId: string): Promise<Device | undefined> {
const userId = this.client.getSafeUserId();
const ownDeviceInfo = await this.client.getCrypto()!.getUserDeviceInfo([userId]);
return ownDeviceInfo.get(userId)?.get(deviceId);
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
this.onFailure?.(reason);
await this.channel.cancel(reason);
}
public async close(): Promise<void> {
await this.channel.close();
}
}

View File

@@ -16,20 +16,7 @@ limitations under the License.
export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void;
export type RendezvousFailureReason =
| LegacyRendezvousFailureReason
| MSC4108FailureReason
| ClientRendezvousFailureReason;
export enum LegacyRendezvousFailureReason {
UserDeclined = "user_declined",
Unknown = "unknown",
Expired = "expired",
UserCancelled = "user_cancelled",
UnsupportedAlgorithm = "unsupported_algorithm",
UnsupportedProtocol = "unsupported_protocol",
HomeserverLacksSupport = "homeserver_lacks_support",
}
export type RendezvousFailureReason = MSC4108FailureReason | ClientRendezvousFailureReason;
export enum MSC4108FailureReason {
AuthorizationExpired = "authorization_expired",

View File

@@ -1,261 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { SAS } from "@matrix-org/olm";
import {
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousChannel,
RendezvousCode,
RendezvousError,
RendezvousIntent,
RendezvousTransport,
RendezvousTransportDetails,
} from "../index.ts";
import { decodeBase64, encodeUnpaddedBase64 } from "../../base64.ts";
import { generateDecimalSas } from "../../crypto/verification/SASDecimal.ts";
import { UnstableValue } from "../../NamespacedValue.ts";
const ECDH_V2 = new UnstableValue(
"m.rendezvous.v2.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
);
export interface ECDHv2RendezvousCode extends RendezvousCode {
rendezvous: {
transport: RendezvousTransportDetails;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key: string;
};
}
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
export interface PlainTextPayload {
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key?: string;
}
export interface EncryptedPayload {
iv: string;
ciphertext: string;
}
async function importKey(key: Uint8Array): Promise<CryptoKey> {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const imported = globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
return imported;
}
/**
* Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
* X25519/ECDH key agreement based secure rendezvous channel.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
private olmSAS?: SAS;
private ourPublicKey: Uint8Array;
private aesKey?: CryptoKey;
private connected = false;
public constructor(
private transport: RendezvousTransport<MSC3903ECDHPayload>,
private theirPublicKey?: Uint8Array,
public onFailure?: (reason: RendezvousFailureReason) => void,
) {
this.olmSAS = new global.Olm.SAS();
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
}
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
if (this.transport.ready) {
throw new Error("Code already generated");
}
await this.transport.send({ algorithm: ECDH_V2.name });
const rendezvous: ECDHv2RendezvousCode = {
rendezvous: {
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
transport: await this.transport.details(),
},
intent,
};
return rendezvous;
}
public async connect(): Promise<string> {
if (this.connected) {
throw new Error("Channel already connected");
}
if (!this.olmSAS) {
throw new Error("Channel closed");
}
const isInitiator = !this.theirPublicKey;
if (isInitiator) {
// wait for the other side to send us their public key
const rawRes = await this.transport.receive();
if (!rawRes) {
throw new Error("No response from other device");
}
const res = rawRes as Partial<PlainTextPayload>;
const { key, algorithm } = res;
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
throw new RendezvousError(
"Unsupported algorithm: " + algorithm,
RendezvousFailureReason.UnsupportedAlgorithm,
);
}
this.theirPublicKey = decodeBase64(key);
} else {
// send our public key unencrypted
await this.transport.send({
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
});
}
this.connected = true;
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
let aesInfo = ECDH_V2.name;
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
this.aesKey = await importKey(aesKeyBytes);
// blank the bytes out to make sure not kept in memory
aesKeyBytes.fill(0);
const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5);
return generateDecimalSas(Array.from(rawChecksum)).join("-");
}
private async encrypt(data: T): Promise<MSC3903ECDHPayload> {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const iv = new Uint8Array(32);
globalThis.crypto.getRandomValues(iv);
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
tagLength: 128,
},
this.aesKey as CryptoKey,
encodedData,
);
return {
iv: encodeUnpaddedBase64(iv),
ciphertext: encodeUnpaddedBase64(ciphertext),
};
}
public async send(payload: T): Promise<void> {
if (!this.olmSAS) {
throw new Error("Channel closed");
}
if (!this.aesKey) {
throw new Error("Shared secret not set up");
}
return this.transport.send(await this.encrypt(payload));
}
private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise<Partial<T>> {
if (!ciphertext || !iv) {
throw new Error("Missing ciphertext and/or iv");
}
const ciphertextBytes = decodeBase64(ciphertext);
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: decodeBase64(iv),
tagLength: 128,
},
this.aesKey as CryptoKey,
ciphertextBytes,
);
return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext)));
}
public async receive(): Promise<Partial<T> | undefined> {
if (!this.olmSAS) {
throw new Error("Channel closed");
}
if (!this.aesKey) {
throw new Error("Shared secret not set up");
}
const rawData = await this.transport.receive();
if (!rawData) {
return undefined;
}
const data = rawData as Partial<EncryptedPayload>;
if (data.ciphertext && data.iv) {
return this.decrypt(data as EncryptedPayload);
}
throw new Error("Data received but no ciphertext");
}
public async close(): Promise<void> {
if (this.olmSAS) {
this.olmSAS.free();
this.olmSAS = undefined;
}
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
try {
await this.transport.cancel(reason);
} finally {
await this.close();
}
}
}

View File

@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3903ECDHv2RendezvousChannel.ts";
export * from "./MSC4108SecureChannel.ts";

View File

@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3906Rendezvous.ts";
export * from "./MSC4108SignInWithQR.ts";
export type * from "./RendezvousChannel.ts";
export type * from "./RendezvousCode.ts";

View File

@@ -1,196 +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.
*/
import { UnstableValue } from "matrix-events-sdk";
import { logger } from "../../logger.ts";
import { sleep } from "../../utils.ts";
import {
RendezvousFailureListener,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousTransport,
RendezvousTransportDetails,
} from "../index.ts";
import { MatrixClient } from "../../matrix.ts";
import { ClientPrefix } from "../../http-api/index.ts";
const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1");
export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails {
uri: string;
}
/**
* Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
* simple HTTP rendezvous protocol.
* Note that this is UNSTABLE and may have breaking changes without notice.
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
* However, we want to keep this implementation around for some time.
* TODO: define an end-of-life date for this implementation.
*/
export class MSC3886SimpleHttpRendezvousTransport<T extends {}> implements RendezvousTransport<T> {
private uri?: string;
private etag?: string;
private expiresAt?: Date;
private client: MatrixClient;
private fallbackRzServer?: string;
private fetchFn?: typeof global.fetch;
private cancelled = false;
private _ready = false;
public onFailure?: RendezvousFailureListener;
public constructor({
onFailure,
client,
fallbackRzServer,
fetchFn,
}: {
fetchFn?: typeof global.fetch;
onFailure?: RendezvousFailureListener;
client: MatrixClient;
fallbackRzServer?: string;
}) {
this.fetchFn = fetchFn;
this.onFailure = onFailure;
this.client = client;
this.fallbackRzServer = fallbackRzServer;
}
public get ready(): boolean {
return this._ready;
}
public async details(): Promise<MSC3886SimpleHttpRendezvousTransportDetails> {
if (!this.uri) {
throw new Error("Rendezvous not set up");
}
return {
type: TYPE.name,
uri: this.uri,
};
}
private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.fetchFn) {
return this.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
private async getPostEndpoint(): Promise<string | undefined> {
try {
if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) {
return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`;
}
} catch (err) {
logger.warn("Failed to get unstable features", err);
}
return this.fallbackRzServer;
}
public async send(data: T): Promise<void> {
if (this.cancelled) {
return;
}
const method = this.uri ? "PUT" : "POST";
const uri = this.uri ?? (await this.getPostEndpoint());
if (!uri) {
throw new Error("Invalid rendezvous URI");
}
const headers: Record<string, string> = { "content-type": "application/json" };
if (this.etag) {
headers["if-match"] = this.etag;
}
const res = await this.fetch(uri, { method, headers, body: JSON.stringify(data) });
if (res.status === 404) {
return this.cancel(RendezvousFailureReason.Unknown);
}
this.etag = res.headers.get("etag") ?? undefined;
if (method === "POST") {
const location = res.headers.get("location");
if (!location) {
throw new Error("No rendezvous URI given");
}
const expires = res.headers.get("expires");
if (expires) {
this.expiresAt = new Date(expires);
}
// we would usually expect the final `url` to be set by a proper fetch implementation.
// however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback
const baseUrl = res.url ?? uri;
// resolve location header which could be relative or absolute
this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href;
this._ready = true;
}
}
public async receive(): Promise<Partial<T> | undefined> {
if (!this.uri) {
throw new Error("Rendezvous not set up");
}
// eslint-disable-next-line no-constant-condition
while (true) {
if (this.cancelled) {
return undefined;
}
const headers: Record<string, string> = {};
if (this.etag) {
headers["if-none-match"] = this.etag;
}
const poll = await this.fetch(this.uri, { method: "GET", headers });
if (poll.status === 404) {
this.cancel(RendezvousFailureReason.Unknown);
return undefined;
}
// rely on server expiring the channel rather than checking ourselves
if (poll.headers.get("content-type") !== "application/json") {
this.etag = poll.headers.get("etag") ?? undefined;
} else if (poll.status === 200) {
this.etag = poll.headers.get("etag") ?? undefined;
return poll.json();
}
await sleep(1000);
}
}
public async cancel(reason: RendezvousFailureReason): Promise<void> {
if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) {
reason = RendezvousFailureReason.Expired;
}
this.cancelled = true;
this._ready = false;
this.onFailure?.(reason);
if (this.uri && reason === RendezvousFailureReason.UserDeclined) {
try {
await this.fetch(this.uri, { method: "DELETE" });
} catch (e) {
logger.warn(e);
}
}
}
}

View File

@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @deprecated in favour of MSC4108-based implementation
*/
export * from "./MSC3886SimpleHttpRendezvousTransport.ts";
export * from "./MSC4108RendezvousSession.ts";