diff --git a/spec/unit/rendezvous/ecdhv2.spec.ts b/spec/unit/rendezvous/ecdhv2.spec.ts deleted file mode 100644 index 1fd3f7cac18..00000000000 --- a/spec/unit/rendezvous/ecdhv2.spec.ts +++ /dev/null @@ -1,171 +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 "../../olm-loader"; -import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous"; -import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels"; -import { decodeBase64 } from "../../../src/base64"; -import { DummyTransport } from "./DummyTransport"; - -function makeTransport(name: string) { - return new DummyTransport(name, { type: "dummy" }); -} - -describe("ECDHv2", function () { - beforeAll(async function () { - await global.Olm.init(); - }); - - describe("with crypto", () => { - it("initiator wants to sign in", async function () { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); - - expect(aliceChecksum).toEqual(bobChecksum); - - const message = { key: "xxx" }; - 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 () { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); - - expect(aliceChecksum).toEqual(bobChecksum); - - const message = { key: "xxx" }; - 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 = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); - - expect(aliceChecksum).toEqual(bobChecksum); - - await expect(alice.connect()).rejects.toThrow(); - - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); - }); - - it("closed", async function () { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key)); - - const bobChecksum = await bob.connect(); - const aliceChecksum = await alice.connect(); - - expect(aliceChecksum).toEqual(bobChecksum); - - alice.close(); - - await expect(alice.connect()).rejects.toThrow(); - await expect(alice.send({})).rejects.toThrow(); - await expect(alice.receive()).rejects.toThrow(); - - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); - }); - - it("require ciphertext", async function () { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - const bob = new MSC3903ECDHv2RendezvousChannel(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({ iv: "dummy", ciphertext: "dummy" }); - - await alice.cancel(RendezvousFailureReason.Unknown); - await bob.cancel(RendezvousFailureReason.Unknown); - await expect(bob.receive()).rejects.toThrow(); - }); - - it("ciphertext before set up", async function () { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob"); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is signing in initiates and generates a code - const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport); - await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE); - - await bobTransport.send({ iv: "dummy", ciphertext: "dummy" }); - - await alice.cancel(RendezvousFailureReason.Unknown); - await expect(alice.receive()).rejects.toThrow(); - }); - }); -}); diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts deleted file mode 100644 index a208ffc590e..00000000000 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ /dev/null @@ -1,712 +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 MockHttpBackend from "matrix-mock-request"; - -import "../../olm-loader"; -import { - MSC3906Rendezvous, - RendezvousCode, - LegacyRendezvousFailureReason as RendezvousFailureReason, - RendezvousIntent, -} from "../../../src/rendezvous"; -import { - ECDHv2RendezvousCode as ECDHRendezvousCode, - MSC3903ECDHPayload, - MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel, -} from "../../../src/rendezvous/channels"; -import { Device, MatrixClient } from "../../../src"; -import { - MSC3886SimpleHttpRendezvousTransport, - MSC3886SimpleHttpRendezvousTransportDetails, -} from "../../../src/rendezvous/transports"; -import { DummyTransport } from "./DummyTransport"; -import { decodeBase64 } from "../../../src/base64"; -import { logger } from "../../../src/logger"; -import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api"; - -type UserID = string; -type DeviceID = string; -type Fingerprint = string; -type SimpleDeviceMap = Record>; - -function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device { - return { - deviceId, - userId, - getFingerprint: () => fingerprint, - } as unknown as Device; -} - -function mockDeviceMap( - userId: UserID, - deviceId: DeviceID, - deviceKey?: Fingerprint, - otherDevices: SimpleDeviceMap = {}, -): Map> { - const deviceMap: Map> = new Map(); - - const myDevices: Map = new Map(); - if (deviceKey) { - myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey)); - } - deviceMap.set(userId, myDevices); - - for (const u in otherDevices) { - let userDevices = deviceMap.get(u); - if (!userDevices) { - userDevices = new Map(); - deviceMap.set(u, userDevices); - } - for (const d in otherDevices[u]) { - userDevices.set(d, mockDevice(u, d, otherDevices[u][d])); - } - } - - return deviceMap; -} - -function makeMockClient(opts: { - userId: UserID; - deviceId: DeviceID; - deviceKey?: Fingerprint; - getLoginTokenEnabled: boolean; - msc3882r0Only: boolean; - msc3886Enabled: boolean; - devices?: SimpleDeviceMap; - verificationFunction?: ( - userId: string, - deviceId: string, - verified: boolean, - blocked: boolean, - known: boolean, - ) => void; - crossSigningIds?: Partial>; -}): [MatrixClient, Map>] { - const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices); - return [ - { - doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => { - if (feature === "org.matrix.msc3886") { - return opts.msc3886Enabled; - } else if (feature === "org.matrix.msc3882") { - return opts.getLoginTokenEnabled; - } else { - return false; - } - }), - getVersions() { - return { - unstable_features: { - "org.matrix.msc3882": opts.getLoginTokenEnabled, - "org.matrix.msc3886": opts.msc3886Enabled, - }, - }; - }, - getCachedCapabilities() { - return opts.msc3882r0Only - ? {} - : { - capabilities: { - "m.get_login_token": { - enabled: opts.getLoginTokenEnabled, - }, - }, - }; - }, - getUserId() { - return opts.userId; - }, - getSafeUserId() { - return opts.userId; - }, - getDeviceId() { - return opts.deviceId; - }, - baseUrl: "https://example.com", - getCrypto() { - return { - getUserDeviceInfo( - [userId]: string[], - downloadUncached?: boolean, - ): Promise>> { - return Promise.resolve(deviceMap); - }, - getCrossSigningKeyId(key: CrossSigningKey): string | null { - return opts.crossSigningIds?.[key] ?? null; - }, - setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise { - return Promise.resolve(); - }, - crossSignDevice(deviceId: string): Promise { - return Promise.resolve(); - }, - getOwnDeviceKeys(): Promise { - return Promise.resolve({ - ed25519: opts.deviceKey!, - curve25519: "aaaa", - }); - }, - }; - }, - } as unknown as MatrixClient, - deviceMap, - ]; -} - -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(); - }); - - let httpBackend: MockHttpBackend; - let fetchFn: typeof global.fetch; - let transports: DummyTransport[]; - const userId: UserID = "@user:example.com"; - - beforeEach(function () { - httpBackend = new MockHttpBackend(); - fetchFn = httpBackend.fetchFn as typeof global.fetch; - transports = []; - }); - - afterEach(function () { - transports.forEach((x) => x.cleanup()); - }); - - it("generate and cancel", async function () { - const [alice] = makeMockClient({ - userId, - deviceId: "ALICE", - msc3886Enabled: false, - getLoginTokenEnabled: true, - msc3882r0Only: 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 aliceEcdh = new MSC3903ECDHRendezvousChannel(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("org.matrix.msc3903.rendezvous.v2.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", - ); - - httpBackend.when("DELETE", "https://fallbackserver/rz").response = { - body: null, - response: { - statusCode: 204, - headers: {}, - }, - }; - - const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined); - await httpBackend.flush(""); - await expect(cancelPromise).resolves.toBeUndefined(); - httpBackend.verifyNoOutstandingExpectation(); - httpBackend.verifyNoOutstandingRequests(); - - await aliceRz.close(); - }); - - async function testNoProtocols({ - getLoginTokenEnabled, - msc3882r0Only, - }: { - getLoginTokenEnabled: boolean; - msc3882r0Only: boolean; - }) { - const aliceTransport = makeTransport("Alice"); - const bobTransport = makeTransport("Bob", "https://test.rz/999999"); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const [alice] = makeMockClient({ - userId, - deviceId: "ALICE", - msc3886Enabled: false, - getLoginTokenEnabled, - msc3882r0Only, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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", - }); - })(); - - await aliceStartProm; - await bobStartPromise; - } - - it("no protocols - r0", async function () { - await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true }); - }); - - it("no protocols - stable", async function () { - await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false }); - }); - - it("new device declines protocol with outcome unsupported", async function () { - 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; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const [alice] = makeMockClient({ - userId, - deviceId: "ALICE", - getLoginTokenEnabled: true, - msc3882r0Only: false, - msc3886Enabled: false, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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: ["org.matrix.msc3906.login_token"], - }); - - await bobEcdh.send({ type: "m.login.finish", outcome: "unsupported" }); - })(); - - await aliceStartProm; - await bobStartPromise; - - expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm); - }); - - it("new device requests an invalid protocol", async function () { - 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; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const [alice] = makeMockClient({ - userId, - deviceId: "ALICE", - getLoginTokenEnabled: true, - msc3882r0Only: false, - msc3886Enabled: false, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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: ["org.matrix.msc3906.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 = makeTransport("Alice", "https://test.rz/123456"); - const bobTransport = makeTransport("Bob", "https://test.rz/999999"); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const [alice] = makeMockClient({ - userId: "alice", - deviceId: "ALICE", - getLoginTokenEnabled: true, - msc3882r0Only: false, - msc3886Enabled: false, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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: ["org.matrix.msc3906.login_token"], - }); - - await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.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 = makeTransport("Alice", "https://test.rz/123456"); - const bobTransport = makeTransport("Bob", "https://test.rz/999999"); - transports.push(aliceTransport, bobTransport); - aliceTransport.otherParty = bobTransport; - bobTransport.otherParty = aliceTransport; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const [alice] = makeMockClient({ - userId: "alice", - deviceId: "ALICE", - getLoginTokenEnabled: true, - msc3882r0Only: false, - msc3886Enabled: false, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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: ["org.matrix.msc3906.login_token"], - }); - - await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" }); - })(); - - await aliceStartProm; - await bobStartPromise; - - const confirmProm = aliceRz.approveLoginOnExistingDevice("token"); - - 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; - }); - - async function completeLogin(devices: SimpleDeviceMap) { - 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; - - // alice is already signs in and generates a code - const aliceOnFailure = jest.fn(); - const aliceVerification = jest.fn(); - const [alice, deviceMap] = makeMockClient({ - userId, - deviceId: "ALICE", - getLoginTokenEnabled: true, - msc3882r0Only: false, - msc3886Enabled: false, - devices, - deviceKey: "aaaa", - verificationFunction: aliceVerification, - crossSigningIds: { - master: "mmmmm", - }, - }); - const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); - const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); - aliceTransport.onCancelled = aliceOnFailure; - await aliceRz.generateCode(); - const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode; - - 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 MSC3903ECDHRendezvousChannel( - 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: ["org.matrix.msc3906.login_token"], - }); - - await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" }); - })(); - - await aliceStartProm; - await bobStartPromise; - - const confirmProm = aliceRz.approveLoginOnExistingDevice("token"); - - 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; - - return { - aliceTransport, - aliceEcdh, - aliceRz, - bobTransport, - bobEcdh, - deviceMap, - }; - } - - it("approve on existing device + verification", async function () { - const { bobEcdh, aliceRz } = await completeLogin({ - [userId]: { - BOB: "bbbb", - }, - }); - 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; - }); - - it("device not online within timeout", async function () { - const { aliceRz } = await completeLogin({}); - await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(); - }); - - it("device appears online within timeout", async function () { - const devices: SimpleDeviceMap = {}; - const { aliceRz, deviceMap } = await completeLogin(devices); - // device appears before the timeout - setTimeout(() => { - deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb")); - }, 1000); - await aliceRz.verifyNewDeviceOnExistingDevice(2000); - }); - - it("device appears online after timeout", async function () { - const devices: SimpleDeviceMap = {}; - const { aliceRz, deviceMap } = await completeLogin(devices); - // device appears after the timeout - setTimeout(() => { - deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb")); - }, 1500); - await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(); - }); - - it("mismatched device key", async function () { - const { aliceRz } = await completeLogin({ - [userId]: { - BOB: "XXXX", - }, - }); - await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/); - }); -}); diff --git a/spec/unit/rendezvous/simpleHttpTransport.spec.ts b/spec/unit/rendezvous/simpleHttpTransport.spec.ts deleted file mode 100644 index c736d4d115d..00000000000 --- a/spec/unit/rendezvous/simpleHttpTransport.spec.ts +++ /dev/null @@ -1,453 +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 MockHttpBackend from "matrix-mock-request"; - -import type { MatrixClient } from "../../../src"; -import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous"; -import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports"; - -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; - - beforeEach(function () { - httpBackend = new MockHttpBackend(); - fetchFn = httpBackend.fetchFn as typeof global.fetch; - }); - - async function postAndCheckLocation( - msc3886Enabled: boolean, - fallbackRzServer: string, - locationResponse: string, - expectedFinalLocation: string, - ) { - 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({}); - httpBackend.when("POST", expectedPostLocation).response = { - body: null, - response: { - statusCode: 201, - headers: { - location: locationResponse, - }, - }, - }; - 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 = { - 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", async function () { - const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn }); - await expect(simpleHttpTransport.send({})).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, - }); - const prom = simpleHttpTransport.send({}); - 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 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({}); - httpBackend.when("POST", "https://fallbackserver/rz").response = { - body: null, - response: { - statusCode: 201, - headers: {}, - }, - }; - await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]); - }); - - it("POST with absolute path response", async function () { - 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 including parent", async function () { - 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, - }); - const prom = simpleHttpTransport.send({}); - 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 client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ - client, - fallbackRzServer: "https://fallbackserver/rz", - fetchFn, - }); - { - // initial POST - 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" }); - }).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 }) => { - 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 client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false }); - const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ - client, - fallbackRzServer: "https://fallbackserver/rz", - fetchFn, - }); - { - // initial POST - 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" }); - }).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({ a: "b" }); - httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => { - expect(headers["if-match"]).toBeUndefined(); - expect(data).toEqual({ a: "b" }); - }).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.send({ c: "d" }); - 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("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, - }); - { - // Create - 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" }); - }).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; - } - }); - - 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, - }); - await expect(simpleHttpTransport.details()).rejects.toThrow(); - }); - - 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); - await expect(simpleHttpTransport.send({})).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, - }); - await expect(simpleHttpTransport.receive()).rejects.toThrow(); - }); - - 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, - }); - - httpBackend.when("POST", "https://fallbackserver/rz").response = { - body: null, - response: { - statusCode: 404, - headers: {}, - }, - }; - await Promise.all([ - expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(), - httpBackend.flush("", 1), - ]); - expect(onFailure).toHaveBeenCalledWith(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({ 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 - httpBackend.when("GET", "https://fallbackserver/rz/123").response = { - body: { foo: "baa" }, - response: { - statusCode: 404, - headers: {}, - }, - }; - await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]); - expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired); - } - }); -}); diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts deleted file mode 100644 index 8fa301f82f2..00000000000 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ /dev/null @@ -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, - private client: MatrixClient, - public onFailure?: RendezvousFailureListener, - ) {} - - /** - * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. - */ - public get code(): string | undefined { - return this._code; - } - - /** - * Generate the code including doing partial set up of the channel where required. - */ - public async generateCode(): Promise { - if (this._code) { - return; - } - - this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); - } - - public async startAfterShowingCode(): Promise { - const checksum = await this.channel.connect(); - - logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); - - // 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(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 { - return (await this.channel.receive()) as MSC3906RendezvousPayload; - } - - private async send(payload: MSC3906RendezvousPayload): Promise { - await this.channel.send(payload); - } - - public async declineLoginOnExistingDevice(): Promise { - logger.info("User declined sign in"); - await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined }); - } - - public async approveLoginOnExistingDevice(loginToken: string): Promise { - // eslint-disable-next-line camelcase - await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl }); - - logger.info("Waiting for outcome"); - const res = await this.receive(); - if (!res) { - return undefined; - } - const { outcome, device_id: deviceId, device_key: deviceKey } = res; - - if (outcome !== "success") { - throw new Error("Linking failed"); - } - - this.newDeviceId = deviceId; - this.newDeviceKey = deviceKey; - - return deviceId; - } - - private async verifyAndCrossSignDevice(deviceInfo: Device): Promise { - 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 { - 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 { - 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 { - this.onFailure?.(reason); - await this.channel.cancel(reason); - } - - public async close(): Promise { - await this.channel.close(); - } -} diff --git a/src/rendezvous/RendezvousFailureReason.ts b/src/rendezvous/RendezvousFailureReason.ts index 7a0116ca0e1..2369a58c0f5 100644 --- a/src/rendezvous/RendezvousFailureReason.ts +++ b/src/rendezvous/RendezvousFailureReason.ts @@ -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", diff --git a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts b/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts deleted file mode 100644 index 47b8e3fd8dc..00000000000 --- a/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts +++ /dev/null @@ -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 { - 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 implements RendezvousChannel { - private olmSAS?: SAS; - private ourPublicKey: Uint8Array; - private aesKey?: CryptoKey; - private connected = false; - - public constructor( - private transport: RendezvousTransport, - private theirPublicKey?: Uint8Array, - public onFailure?: (reason: RendezvousFailureReason) => void, - ) { - this.olmSAS = new global.Olm.SAS(); - this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey()); - } - - public async generateCode(intent: RendezvousIntent): Promise { - if (this.transport.ready) { - throw new Error("Code already generated"); - } - - await this.transport.send({ algorithm: ECDH_V2.name }); - - const rendezvous: ECDHv2RendezvousCode = { - rendezvous: { - algorithm: ECDH_V2.name, - key: encodeUnpaddedBase64(this.ourPublicKey), - transport: await this.transport.details(), - }, - intent, - }; - - return rendezvous; - } - - public async connect(): Promise { - if (this.connected) { - throw new Error("Channel already connected"); - } - - if (!this.olmSAS) { - throw new Error("Channel closed"); - } - - const isInitiator = !this.theirPublicKey; - - if (isInitiator) { - // wait for the other side to send us their public key - const rawRes = await this.transport.receive(); - if (!rawRes) { - throw new Error("No response from other device"); - } - const res = rawRes as Partial; - const { key, algorithm } = res; - if (!algorithm || !ECDH_V2.matches(algorithm) || !key) { - throw new RendezvousError( - "Unsupported algorithm: " + algorithm, - RendezvousFailureReason.UnsupportedAlgorithm, - ); - } - - this.theirPublicKey = decodeBase64(key); - } else { - // send our public key unencrypted - await this.transport.send({ - algorithm: ECDH_V2.name, - key: encodeUnpaddedBase64(this.ourPublicKey), - }); - } - - this.connected = true; - - this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!)); - - const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!; - const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey; - let aesInfo = ECDH_V2.name; - aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`; - aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`; - - const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); - - this.aesKey = await importKey(aesKeyBytes); - - // blank the bytes out to make sure not kept in memory - aesKeyBytes.fill(0); - - const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); - return generateDecimalSas(Array.from(rawChecksum)).join("-"); - } - - private async encrypt(data: T): Promise { - if (!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 { - if (!this.olmSAS) { - throw new Error("Channel closed"); - } - - if (!this.aesKey) { - throw new Error("Shared secret not set up"); - } - - return this.transport.send(await this.encrypt(payload)); - } - - private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise> { - if (!ciphertext || !iv) { - throw new Error("Missing ciphertext and/or iv"); - } - - const ciphertextBytes = decodeBase64(ciphertext); - - if (!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 | undefined> { - if (!this.olmSAS) { - throw new Error("Channel closed"); - } - if (!this.aesKey) { - throw new Error("Shared secret not set up"); - } - - const rawData = await this.transport.receive(); - if (!rawData) { - return undefined; - } - const data = rawData as Partial; - if (data.ciphertext && data.iv) { - return this.decrypt(data as EncryptedPayload); - } - - throw new Error("Data received but no ciphertext"); - } - - public async close(): Promise { - if (this.olmSAS) { - this.olmSAS.free(); - this.olmSAS = undefined; - } - } - - public async cancel(reason: RendezvousFailureReason): Promise { - try { - await this.transport.cancel(reason); - } finally { - await this.close(); - } - } -} diff --git a/src/rendezvous/channels/index.ts b/src/rendezvous/channels/index.ts index c525e278485..113e890438f 100644 --- a/src/rendezvous/channels/index.ts +++ b/src/rendezvous/channels/index.ts @@ -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"; diff --git a/src/rendezvous/index.ts b/src/rendezvous/index.ts index 95fa72d6187..498f060e33e 100644 --- a/src/rendezvous/index.ts +++ b/src/rendezvous/index.ts @@ -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"; diff --git a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts deleted file mode 100644 index b8521eda420..00000000000 --- a/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts +++ /dev/null @@ -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 implements RendezvousTransport { - private uri?: string; - private etag?: string; - private expiresAt?: Date; - private client: MatrixClient; - private fallbackRzServer?: string; - private fetchFn?: typeof global.fetch; - private cancelled = false; - private _ready = false; - public onFailure?: RendezvousFailureListener; - - public constructor({ - onFailure, - client, - fallbackRzServer, - fetchFn, - }: { - fetchFn?: typeof global.fetch; - onFailure?: RendezvousFailureListener; - client: MatrixClient; - fallbackRzServer?: string; - }) { - this.fetchFn = fetchFn; - this.onFailure = onFailure; - this.client = client; - this.fallbackRzServer = fallbackRzServer; - } - - public get ready(): boolean { - return this._ready; - } - - public async details(): Promise { - if (!this.uri) { - throw new Error("Rendezvous not set up"); - } - - return { - type: TYPE.name, - uri: this.uri, - }; - } - - private fetch(resource: URL | string, options?: RequestInit): ReturnType { - if (this.fetchFn) { - return this.fetchFn(resource, options); - } - return global.fetch(resource, options); - } - - private async getPostEndpoint(): Promise { - try { - if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) { - return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; - } - } catch (err) { - logger.warn("Failed to get unstable features", err); - } - - return this.fallbackRzServer; - } - - public async send(data: T): Promise { - if (this.cancelled) { - return; - } - const method = this.uri ? "PUT" : "POST"; - const uri = this.uri ?? (await this.getPostEndpoint()); - - if (!uri) { - throw new Error("Invalid rendezvous URI"); - } - - const headers: Record = { "content-type": "application/json" }; - if (this.etag) { - headers["if-match"] = this.etag; - } - - const res = await this.fetch(uri, { method, headers, body: JSON.stringify(data) }); - if (res.status === 404) { - return this.cancel(RendezvousFailureReason.Unknown); - } - this.etag = res.headers.get("etag") ?? undefined; - - if (method === "POST") { - const location = res.headers.get("location"); - if (!location) { - throw new Error("No rendezvous URI given"); - } - const expires = res.headers.get("expires"); - if (expires) { - this.expiresAt = new Date(expires); - } - // we would usually expect the final `url` to be set by a proper fetch implementation. - // however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback - const baseUrl = res.url ?? uri; - // resolve location header which could be relative or absolute - this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href; - this._ready = true; - } - } - - public async receive(): Promise | undefined> { - if (!this.uri) { - throw new Error("Rendezvous not set up"); - } - // eslint-disable-next-line no-constant-condition - while (true) { - if (this.cancelled) { - return undefined; - } - - const headers: Record = {}; - if (this.etag) { - headers["if-none-match"] = this.etag; - } - const poll = await this.fetch(this.uri, { method: "GET", headers }); - - if (poll.status === 404) { - this.cancel(RendezvousFailureReason.Unknown); - return undefined; - } - - // rely on server expiring the channel rather than checking ourselves - - if (poll.headers.get("content-type") !== "application/json") { - this.etag = poll.headers.get("etag") ?? undefined; - } else if (poll.status === 200) { - this.etag = poll.headers.get("etag") ?? undefined; - return poll.json(); - } - await sleep(1000); - } - } - - public async cancel(reason: RendezvousFailureReason): Promise { - if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) { - reason = RendezvousFailureReason.Expired; - } - - this.cancelled = true; - this._ready = false; - this.onFailure?.(reason); - - if (this.uri && reason === RendezvousFailureReason.UserDeclined) { - try { - await this.fetch(this.uri, { method: "DELETE" }); - } catch (e) { - logger.warn(e); - } - } - } -} diff --git a/src/rendezvous/transports/index.ts b/src/rendezvous/transports/index.ts index 88fe91ba1d9..64aa2b00430 100644 --- a/src/rendezvous/transports/index.ts +++ b/src/rendezvous/transports/index.ts @@ -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";