You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 (#2747)
* Clean implementation of MSC3886 and MSC3903 * Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better * Start of some unit tests * Make AES work on Node.js as well as browser * Tests for ECDH/X25519 * stric mode linting * Fix incorrect test * Refactor full rendezvous logic out of react-sdk into js-sdk * Use correct unstable import * Pass fetch around * Make correct usage of fetch in tests * fix: you can't call fetch when it's not on window * Use class names to make it clearer that these are unstable MSC implementations * Linting * Clean implementation of MSC3886 and MSC3903 * Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better * Start of some unit tests * Make AES work on Node.js as well as browser * Tests for ECDH/X25519 * stric mode linting * Fix incorrect test * Refactor full rendezvous logic out of react-sdk into js-sdk * Use correct unstable import * Pass fetch around * Make correct usage of fetch in tests * fix: you can't call fetch when it's not on window * Use class names to make it clearer that these are unstable MSC implementations * Linting * Reduce log noise * Tidy up interface a bit * Additional test for transport layer * Linting * Refactor dummy transport to be re-usable * Remove redundant condition * Handle more error cases * Initial tests for MSC3906 * Reduce scope of PR to only cover generating a code on existing device * Strict linting * Additional test cases * Lint * additional test cases and remove some code smells * More test cases * Strict lint * Strict lint * Test case * Refactor to handle UIA * Unstable prefixes * Lint * Missed due to lack of strict... * Test server capabilities using Feature * Remove redundant assignment * Refactor ro resuse generateDecimal from SAS * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/rendezvous/channels/ecdhV1.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Rename files to titlecase * Visibility modifiers * Resolve public mutability * Refactor logic to reduce duplication * Refactor to have better defined data types throughout * Rebase and remove Node.js crypto * Wipe AES key out after use * Add typing for MSC3906 layer * Strict lint * Fix double connect detection * Remove unintended debug statement * Return types * Use generics * Make type of MSC3903ECDHPayload explicit * Use unstable prefix for RendezvousChannelAlgorithm * Fix * Extra unstable type * Test types Co-authored-by: Travis Ralston <travisr@matrix.org> Co-authored-by: Kerry <kerrya@element.io>
This commit is contained in:
92
spec/unit/rendezvous/DummyTransport.ts
Normal file
92
spec/unit/rendezvous/DummyTransport.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
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 "../../../src/logger";
|
||||||
|
import {
|
||||||
|
RendezvousFailureListener,
|
||||||
|
RendezvousFailureReason,
|
||||||
|
RendezvousTransport,
|
||||||
|
RendezvousTransportDetails,
|
||||||
|
} from "../../../src/rendezvous";
|
||||||
|
import { sleep } from '../../../src/utils';
|
||||||
|
|
||||||
|
export class DummyTransport<D extends RendezvousTransportDetails, T> implements RendezvousTransport<T> {
|
||||||
|
otherParty?: DummyTransport<D, T>;
|
||||||
|
etag?: string;
|
||||||
|
lastEtagReceived?: string;
|
||||||
|
data: T | undefined;
|
||||||
|
|
||||||
|
ready = false;
|
||||||
|
cancelled = false;
|
||||||
|
|
||||||
|
constructor(private name: string, private mockDetails: D) {}
|
||||||
|
onCancelled?: RendezvousFailureListener;
|
||||||
|
|
||||||
|
details(): Promise<RendezvousTransportDetails> {
|
||||||
|
return Promise.resolve(this.mockDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(data: T): Promise<void> {
|
||||||
|
logger.info(
|
||||||
|
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
|
||||||
|
JSON.stringify(data)} where etag matches ${this.etag}`,
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(): Promise<T | undefined> {
|
||||||
|
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;
|
||||||
|
logger.info(
|
||||||
|
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
|
||||||
|
`${JSON.stringify(this.data)} with etag ${this.etag}`,
|
||||||
|
);
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
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<void> {
|
||||||
|
this.cancelled = true;
|
||||||
|
this.onCancelled?.(reason);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.cancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
spec/unit/rendezvous/ecdh.spec.ts
Normal file
172
spec/unit/rendezvous/ecdh.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
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 { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||||
|
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels';
|
||||||
|
import { decodeBase64 } from '../../../src/crypto/olmlib';
|
||||||
|
import { DummyTransport } from './DummyTransport';
|
||||||
|
|
||||||
|
function makeTransport(name: string) {
|
||||||
|
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'dummy' });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ECDHv1', 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 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);
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
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 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 = makeTransport('Alice');
|
||||||
|
const bobTransport = makeTransport('Bob');
|
||||||
|
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 = makeTransport('Alice');
|
||||||
|
const bobTransport = makeTransport('Bob');
|
||||||
|
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({ iv: "dummy", ciphertext: "dummy" });
|
||||||
|
expect(bob.receive()).rejects.toThrowError();
|
||||||
|
|
||||||
|
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||||
|
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||||
|
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||||
|
|
||||||
|
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||||
|
|
||||||
|
expect(alice.receive()).rejects.toThrowError();
|
||||||
|
|
||||||
|
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
602
spec/unit/rendezvous/rendezvous.spec.ts
Normal file
602
spec/unit/rendezvous/rendezvous.spec.ts
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
RendezvousFailureReason,
|
||||||
|
RendezvousIntent,
|
||||||
|
} from "../../../src/rendezvous";
|
||||||
|
import {
|
||||||
|
ECDHv1RendezvousCode,
|
||||||
|
MSC3903ECDHPayload,
|
||||||
|
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<string, Partial<DeviceInfo>>;
|
||||||
|
verificationFunction?: (
|
||||||
|
userId: string, deviceId: string, verified: boolean, blocked: boolean, known: boolean,
|
||||||
|
) => void;
|
||||||
|
crossSigningIds?: Record<string, string>;
|
||||||
|
}): MatrixClient {
|
||||||
|
return {
|
||||||
|
getVersions() {
|
||||||
|
return {
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3882": opts.msc3882Enabled,
|
||||||
|
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getUserId() { return opts.userId; },
|
||||||
|
getDeviceId() { return opts.deviceId; },
|
||||||
|
getDeviceEd25519Key() { return opts.deviceKey; },
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTransport(name: string, uri = 'https://test.rz/123456') {
|
||||||
|
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: 'http.v1', uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Rendezvous", function() {
|
||||||
|
beforeAll(async function() {
|
||||||
|
await global.Olm.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
let httpBackend: MockHttpBackend;
|
||||||
|
let fetchFn: typeof global.fetchFn;
|
||||||
|
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||||
|
|
||||||
|
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: "@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 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("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");
|
||||||
|
|
||||||
|
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 = 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: "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',
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
await aliceStartProm;
|
||||||
|
await bobStartPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("new device declines 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: "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: ['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 declines 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: "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: ['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",
|
||||||
|
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: ['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",
|
||||||
|
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: ['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: Record<string, Partial<DeviceInfo>>) {
|
||||||
|
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 = makeMockClient({
|
||||||
|
userId: "alice",
|
||||||
|
deviceId: "ALICE",
|
||||||
|
msc3882Enabled: true,
|
||||||
|
msc3886Enabled: false,
|
||||||
|
devices,
|
||||||
|
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: ['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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("approve on existing device + verification", async function() {
|
||||||
|
const { bobEcdh, aliceRz } = await completeLogin({
|
||||||
|
BOB: {
|
||||||
|
getFingerprint: () => "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({});
|
||||||
|
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("device appears online within timeout", async function() {
|
||||||
|
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||||
|
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<string, Partial<DeviceInfo>> = {};
|
||||||
|
const { aliceRz } = await completeLogin(devices);
|
||||||
|
// device appears after 1 second
|
||||||
|
setTimeout(() => {
|
||||||
|
devices.BOB = {
|
||||||
|
getFingerprint: () => "bbbb",
|
||||||
|
};
|
||||||
|
}, 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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
451
spec/unit/rendezvous/simpleHttpTransport.spec.ts
Normal file
451
spec/unit/rendezvous/simpleHttpTransport.spec.ts
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
/*
|
||||||
|
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 { 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", function() {
|
||||||
|
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||||
|
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||||
|
expect(simpleHttpTransport.send({})).rejects.toThrowError("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({});
|
||||||
|
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,
|
||||||
|
"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 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,
|
||||||
|
});
|
||||||
|
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({})).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({ 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({ 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from './Error';
|
} from './Error';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import { IContent, MatrixEvent } from "../../models/event";
|
import { IContent, MatrixEvent } from "../../models/event";
|
||||||
|
import { generateDecimalSas } from './SASDecimal';
|
||||||
import { EventType } from '../../@types/event';
|
import { EventType } from '../../@types/event';
|
||||||
|
|
||||||
const START_TYPE = EventType.KeyVerificationStart;
|
const START_TYPE = EventType.KeyVerificationStart;
|
||||||
@@ -52,22 +53,6 @@ const newMismatchedCommitmentError = errorFactory(
|
|||||||
"m.mismatched_commitment", "Mismatched commitment",
|
"m.mismatched_commitment", "Mismatched commitment",
|
||||||
);
|
);
|
||||||
|
|
||||||
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];
|
type EmojiMapping = [emoji: string, name: string];
|
||||||
|
|
||||||
const emojiMapping: EmojiMapping[] = [
|
const emojiMapping: EmojiMapping[] = [
|
||||||
|
|||||||
37
src/crypto/verification/SASDecimal.ts
Normal file
37
src/crypto/verification/SASDecimal.ts
Normal file
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ export enum ServerSupport {
|
|||||||
export enum Feature {
|
export enum Feature {
|
||||||
Thread = "Thread",
|
Thread = "Thread",
|
||||||
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
||||||
|
LoginTokenRequest = "LoginTokenRequest",
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureSupportCondition = {
|
type FeatureSupportCondition = {
|
||||||
@@ -41,6 +42,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
|||||||
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
|
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
|
||||||
matrixVersion: "v1.4",
|
matrixVersion: "v1.4",
|
||||||
},
|
},
|
||||||
|
[Feature.LoginTokenRequest]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3882"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||||
|
|||||||
269
src/rendezvous/MSC3906Rendezvous.ts
Normal file
269
src/rendezvous/MSC3906Rendezvous.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
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 } 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, RendezvousIntent } from ".";
|
||||||
|
|
||||||
|
enum PayloadType {
|
||||||
|
Start = 'm.login.start',
|
||||||
|
Finish = 'm.login.finish',
|
||||||
|
Progress = 'm.login.progress',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Outcome {
|
||||||
|
Success = 'success',
|
||||||
|
Failure = 'failure',
|
||||||
|
Verified = 'verified',
|
||||||
|
Declined = 'declined',
|
||||||
|
Unsupported = 'unsupported',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSC3906RendezvousPayload {
|
||||||
|
type: PayloadType;
|
||||||
|
intent?: RendezvousIntent;
|
||||||
|
outcome?: Outcome;
|
||||||
|
device_id?: string;
|
||||||
|
device_key?: string;
|
||||||
|
verifying_device_id?: string;
|
||||||
|
verifying_device_key?: string;
|
||||||
|
master_key?: string;
|
||||||
|
protocols?: string[];
|
||||||
|
protocol?: string;
|
||||||
|
login_token?: string;
|
||||||
|
homeserver?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc3906.login_token");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements MSC3906 to allow a user to sign in on a new device using QR code.
|
||||||
|
* This implementation only supports generating a QR code on a device that is already signed in.
|
||||||
|
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export class MSC3906Rendezvous {
|
||||||
|
private newDeviceId?: string;
|
||||||
|
private newDeviceKey?: string;
|
||||||
|
private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
|
||||||
|
private _code?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param channel The secure channel used for communication
|
||||||
|
* @param client The Matrix client in used on the device already logged in
|
||||||
|
* @param onFailure Callback for when the rendezvous fails
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private channel: RendezvousChannel<MSC3906RendezvousPayload>,
|
||||||
|
private client: MatrixClient,
|
||||||
|
public onFailure?: RendezvousFailureListener,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
|
||||||
|
*/
|
||||||
|
public get code(): string | undefined {
|
||||||
|
return this._code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the code including doing partial set up of the channel where required.
|
||||||
|
*/
|
||||||
|
public async generateCode(): Promise<void> {
|
||||||
|
if (this._code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startAfterShowingCode(): Promise<string | undefined> {
|
||||||
|
const checksum = await this.channel.connect();
|
||||||
|
|
||||||
|
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
|
||||||
|
|
||||||
|
const features = await buildFeatureSupportMap(await this.client.getVersions());
|
||||||
|
// determine available protocols
|
||||||
|
if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
|
||||||
|
logger.info("Server doesn't support MSC3882");
|
||||||
|
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
|
||||||
|
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
|
||||||
|
|
||||||
|
logger.info('Waiting for other device to chose protocol');
|
||||||
|
const { type, protocol, outcome } = await this.receive();
|
||||||
|
|
||||||
|
if (type === PayloadType.Finish) {
|
||||||
|
// new device decided not to complete
|
||||||
|
switch (outcome ?? '') {
|
||||||
|
case 'unsupported':
|
||||||
|
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await this.cancel(RendezvousFailureReason.Unknown);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== PayloadType.Progress) {
|
||||||
|
await this.cancel(RendezvousFailureReason.Unknown);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) {
|
||||||
|
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async receive(): Promise<MSC3906RendezvousPayload> {
|
||||||
|
return await this.channel.receive() as MSC3906RendezvousPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async send(payload: MSC3906RendezvousPayload) {
|
||||||
|
await this.channel.send(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async declineLoginOnExistingDevice() {
|
||||||
|
logger.info('User declined sign in');
|
||||||
|
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> {
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl });
|
||||||
|
|
||||||
|
logger.info('Waiting for outcome');
|
||||||
|
const res = await this.receive();
|
||||||
|
if (!res) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { outcome, device_id: deviceId, device_key: deviceKey } = res;
|
||||||
|
|
||||||
|
if (outcome !== 'success') {
|
||||||
|
throw new Error('Linking failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newDeviceId = deviceId;
|
||||||
|
this.newDeviceKey = deviceKey;
|
||||||
|
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo): Promise<CrossSigningInfo | DeviceInfo> {
|
||||||
|
if (!this.client.crypto) {
|
||||||
|
throw new Error('Crypto not available on client');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newDeviceId) {
|
||||||
|
throw new Error('No new device ID set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that keys received from the server for the new device match those received from the device itself
|
||||||
|
if (deviceInfo.getFingerprint() !== this.newDeviceKey) {
|
||||||
|
throw new Error(
|
||||||
|
`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = this.client.getUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('No user ID set');
|
||||||
|
}
|
||||||
|
// mark the device as verified locally + cross sign
|
||||||
|
logger.info(`Marking device ${this.newDeviceId} as verified`);
|
||||||
|
const info = await this.client.crypto.setDeviceVerification(
|
||||||
|
userId,
|
||||||
|
this.newDeviceId,
|
||||||
|
true, false, true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master');
|
||||||
|
|
||||||
|
await this.send({
|
||||||
|
type: PayloadType.Finish,
|
||||||
|
outcome: Outcome.Verified,
|
||||||
|
verifying_device_id: this.client.getDeviceId(),
|
||||||
|
verifying_device_key: this.client.getDeviceEd25519Key(),
|
||||||
|
master_key: masterPublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the device and cross-sign it.
|
||||||
|
* @param timeout time in milliseconds to wait for device to come online
|
||||||
|
* @returns the new device info if the device was verified
|
||||||
|
*/
|
||||||
|
public async verifyNewDeviceOnExistingDevice(
|
||||||
|
timeout = 10 * 1000,
|
||||||
|
): Promise<DeviceInfo | CrossSigningInfo | undefined> {
|
||||||
|
if (!this.newDeviceId) {
|
||||||
|
throw new Error('No new device to sign');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newDeviceKey) {
|
||||||
|
logger.info("No new device key to sign");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.client.crypto) {
|
||||||
|
throw new Error('Crypto not available on client');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = this.client.getUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('No user ID set');
|
||||||
|
}
|
||||||
|
|
||||||
|
let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
|
||||||
|
|
||||||
|
if (!deviceInfo) {
|
||||||
|
logger.info("Going to wait for new device to be online");
|
||||||
|
await sleep(timeout);
|
||||||
|
deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceInfo) {
|
||||||
|
return await this.verifyAndCrossSignDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Device not online within timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||||
|
this.onFailure?.(reason);
|
||||||
|
await this.channel.cancel(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
await this.channel.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/rendezvous/RendezvousChannel.ts
Normal file
52
src/rendezvous/RendezvousChannel.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
RendezvousCode,
|
||||||
|
RendezvousIntent,
|
||||||
|
RendezvousFailureReason,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
export interface RendezvousChannel<T> {
|
||||||
|
/**
|
||||||
|
* @returns the checksum/confirmation digits to be shown to the user
|
||||||
|
*/
|
||||||
|
connect(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a payload via the channel.
|
||||||
|
* @param data payload to send
|
||||||
|
*/
|
||||||
|
send(data: T): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive a payload from the channel.
|
||||||
|
* @returns the received payload
|
||||||
|
*/
|
||||||
|
receive(): Promise<Partial<T> | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the channel and clear up any resources.
|
||||||
|
*/
|
||||||
|
close(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a representation of the channel that can be encoded in a QR or similar
|
||||||
|
*/
|
||||||
|
generateCode(intent: RendezvousIntent): Promise<RendezvousCode>;
|
||||||
|
|
||||||
|
cancel(reason: RendezvousFailureReason): Promise<void>;
|
||||||
|
}
|
||||||
25
src/rendezvous/RendezvousCode.ts
Normal file
25
src/rendezvous/RendezvousCode.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RendezvousTransportDetails, RendezvousIntent } from ".";
|
||||||
|
|
||||||
|
export interface RendezvousCode {
|
||||||
|
intent: RendezvousIntent;
|
||||||
|
rendezvous?: {
|
||||||
|
transport: RendezvousTransportDetails;
|
||||||
|
algorithm: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
23
src/rendezvous/RendezvousError.ts
Normal file
23
src/rendezvous/RendezvousError.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RendezvousFailureReason } from ".";
|
||||||
|
|
||||||
|
export class RendezvousError extends Error {
|
||||||
|
constructor(message: string, public readonly code: RendezvousFailureReason) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/rendezvous/RendezvousFailureReason.ts
Normal file
31
src/rendezvous/RendezvousFailureReason.ts
Normal file
@@ -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',
|
||||||
|
}
|
||||||
20
src/rendezvous/RendezvousIntent.ts
Normal file
20
src/rendezvous/RendezvousIntent.ts
Normal file
@@ -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",
|
||||||
|
}
|
||||||
58
src/rendezvous/RendezvousTransport.ts
Normal file
58
src/rendezvous/RendezvousTransport.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RendezvousFailureListener, RendezvousFailureReason } from ".";
|
||||||
|
|
||||||
|
export interface RendezvousTransportDetails {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing a generic rendezvous transport.
|
||||||
|
*/
|
||||||
|
export interface RendezvousTransport<T> {
|
||||||
|
/**
|
||||||
|
* Ready state of the transport. This is set to true when the transport is ready to be used.
|
||||||
|
*/
|
||||||
|
readonly ready: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for cancellation events. This is called when the rendezvous is cancelled or fails.
|
||||||
|
*/
|
||||||
|
onFailure?: RendezvousFailureListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the transport details that can be encoded in a QR or similar
|
||||||
|
*/
|
||||||
|
details(): Promise<RendezvousTransportDetails>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data via the transport.
|
||||||
|
* @param data the data itself
|
||||||
|
*/
|
||||||
|
send(data: T): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive data from the transport.
|
||||||
|
*/
|
||||||
|
receive(): Promise<Partial<T> | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the rendezvous. This will call `onCancelled()` if it is set.
|
||||||
|
* @param reason the reason for the cancellation/failure
|
||||||
|
*/
|
||||||
|
cancel(reason: RendezvousFailureReason): Promise<void>;
|
||||||
|
}
|
||||||
265
src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts
Normal file
265
src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
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 {
|
||||||
|
RendezvousError,
|
||||||
|
RendezvousCode,
|
||||||
|
RendezvousIntent,
|
||||||
|
RendezvousChannel,
|
||||||
|
RendezvousTransportDetails,
|
||||||
|
RendezvousTransport,
|
||||||
|
RendezvousFailureReason,
|
||||||
|
} 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: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
|
||||||
|
|
||||||
|
export interface PlainTextPayload {
|
||||||
|
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncryptedPayload {
|
||||||
|
iv: string;
|
||||||
|
ciphertext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||||
|
if (!subtleCrypto) {
|
||||||
|
throw new Error('Web Crypto is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const imported = subtleCrypto.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
{ name: 'AES-GCM' },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
|
||||||
|
* X25519/ECDH key agreement based secure rendezvous channel.
|
||||||
|
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export class MSC3903ECDHv1RendezvousChannel<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<ECDHv1RendezvousCode> {
|
||||||
|
if (this.transport.ready) {
|
||||||
|
throw new Error('Code already generated');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.transport.send({ algorithm: ECDH_V1.name });
|
||||||
|
|
||||||
|
const rendezvous: ECDHv1RendezvousCode = {
|
||||||
|
"rendezvous": {
|
||||||
|
algorithm: ECDH_V1.name,
|
||||||
|
key: encodeBase64(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_V1.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_V1.name,
|
||||||
|
key: encodeBase64(this.ourPublicKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connected = true;
|
||||||
|
|
||||||
|
this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!));
|
||||||
|
|
||||||
|
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
|
||||||
|
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
|
||||||
|
let aesInfo = ECDH_V1.name;
|
||||||
|
aesInfo += `|${encodeBase64(initiatorKey)}`;
|
||||||
|
aesInfo += `|${encodeBase64(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 (!subtleCrypto) {
|
||||||
|
throw new Error('Web Crypto is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const iv = new Uint8Array(32);
|
||||||
|
crypto.getRandomValues(iv);
|
||||||
|
|
||||||
|
const encodedData = new TextEncoder().encode(JSON.stringify(data));
|
||||||
|
|
||||||
|
const ciphertext = await subtleCrypto.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
tagLength: 128,
|
||||||
|
},
|
||||||
|
this.aesKey as CryptoKey,
|
||||||
|
encodedData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
iv: encodeBase64(iv),
|
||||||
|
ciphertext: encodeBase64(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 (!subtleCrypto) {
|
||||||
|
throw new Error('Web Crypto is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plaintext = await subtleCrypto.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv: decodeBase64(iv),
|
||||||
|
tagLength: 128,
|
||||||
|
},
|
||||||
|
this.aesKey as CryptoKey,
|
||||||
|
ciphertextBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async receive(): Promise<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/rendezvous/channels/index.ts
Normal file
18
src/rendezvous/channels/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
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 './MSC3903ECDHv1RendezvousChannel';
|
||||||
|
|
||||||
23
src/rendezvous/index.ts
Normal file
23
src/rendezvous/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './MSC3906Rendezvous';
|
||||||
|
export * from './RendezvousChannel';
|
||||||
|
export * from './RendezvousCode';
|
||||||
|
export * from './RendezvousError';
|
||||||
|
export * from './RendezvousFailureReason';
|
||||||
|
export * from './RendezvousIntent';
|
||||||
|
export * from './RendezvousTransport';
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UnstableValue } from 'matrix-events-sdk';
|
||||||
|
|
||||||
|
import { logger } from '../../logger';
|
||||||
|
import { sleep } from '../../utils';
|
||||||
|
import {
|
||||||
|
RendezvousFailureListener,
|
||||||
|
RendezvousFailureReason,
|
||||||
|
RendezvousTransport,
|
||||||
|
RendezvousTransportDetails,
|
||||||
|
} from '..';
|
||||||
|
import { MatrixClient } from '../../matrix';
|
||||||
|
import { ClientPrefix } from '../../http-api';
|
||||||
|
|
||||||
|
const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1");
|
||||||
|
|
||||||
|
export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails {
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
|
||||||
|
* simple HTTP rendezvous protocol.
|
||||||
|
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export class MSC3886SimpleHttpRendezvousTransport<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/rendezvous/transports/index.ts
Normal file
17
src/rendezvous/transports/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './MSC3886SimpleHttpRendezvousTransport';
|
||||||
Reference in New Issue
Block a user