diff --git a/package.json b/package.json
index 142b263..c843b19 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@bytemd/plugin-external-links": "^1.3.0",
"@bytemd/plugin-gfm": "^1.10.13",
"@material/typography": "^13.0.0",
- "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
+ "@matrix-org/olm": "3.2.15",
"@smui/button": "^5.0.1",
"@smui/card": "^5.0.1",
"@smui/data-table": "^5.0.1",
@@ -68,6 +68,7 @@
"dayjs": "^1.10.7",
"events": "^3.3.0",
"github-markdown-css": "^5.1.0",
+ "jsqr": "^1.4.0",
"log4js": "^6.4.1",
"matrix-files-sdk": "^3.1.0",
"matrix-js-sdk": "^30.1.0",
diff --git a/src/components/auth/Login.svelte b/src/components/auth/Login.svelte
index f286019..bf7b15c 100644
--- a/src/components/auth/Login.svelte
+++ b/src/components/auth/Login.svelte
@@ -33,6 +33,11 @@ limitations under the License.
import QrCode from "svelte-qrcode";
import { debounce } from '../../utils';
import type { DeviceAuthorizationResponse } from 'oidc-client-ts';
+ import ScanQRCode from "./ScanQRCode.svelte";
+ import { decodeBase64 } from 'matrix-js-sdk/lib/crypto/olmlib';
+ import { type ECDHv2RendezvousCode, MSC3903ECDHv2RendezvousChannel, type MSC3903ECDHPayload } from "matrix-js-sdk/src/rendezvous/channels";
+ import { MSC3886SimpleHttpRendezvousTransport, type MSC3886SimpleHttpRendezvousTransportDetails } from "matrix-js-sdk/src/rendezvous/transports";
+ import { PayloadType, type MSC3906RendezvousPayload, Outcome, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
export let clientManager: ClientManager;
@@ -44,15 +49,33 @@ limitations under the License.
let passwordSupported = false;
let oidcSupported = false;
let deviceFlowSupported = false;
-
+ let scanQrCode = false;
+ let scanQrResult = "";
+ let scanQrChecksum: string[] = [];
let oidcDeviceFlow: DeviceAuthorizationResponse | undefined;
+ let rzChannel: MSC3903ECDHv2RendezvousChannel | undefined;
let homeserverInput = clientManager.homeserverUrl;
let loadedServerInfo = '';
$: params = new URLSearchParams(document.location.search);
+ let dagPrettyUri = '';
+
+ async function oidcDiscovery() {
+ clientManager.oidcIssuer = '';
+ try {
+ const wellKnown = await getWellKnown(clientManager.homeserverUrl);
+ if (wellKnown['m.homeserver']?.base_url && wellKnown['m.homeserver'].base_url !== clientManager.homeserverUrl) {
+ clientManager.homeserverUrl = wellKnown['m.homeserver'].base_url;
+ }
+ clientManager.oidcIssuer = wellKnown['org.matrix.msc2965.authentication']?.issuer ?? '';
+ } catch (e: any) {
+ // OIDC is not supported as no .well-known
+ log.warn(e);
+ }
+ }
$: (async () => {
if (params.has('error_description')) {
errorMessage = params.get('error_description') ?? params.get('error') ?? '';
@@ -62,24 +85,13 @@ limitations under the License.
}
if (params.has('code')) {
// OIDC in progress?
- } else if (loadedServerInfo !== clientManager.homeserverUrl) {
+ } else if (!scanQrCode && loadedServerInfo !== clientManager.homeserverUrl) {
try {
errorMessage = '';
passwordSupported = false;
- oidcSupported = false;
- deviceFlowSupported = false;
- clientManager.oidcIssuer = '';
- try {
- const wellKnown = await getWellKnown(clientManager.homeserverUrl);
- if (wellKnown['m.homeserver']?.base_url && wellKnown['m.homeserver'].base_url !== clientManager.homeserverUrl) {
- clientManager.homeserverUrl = wellKnown['m.homeserver'].base_url;
- }
- clientManager.oidcIssuer = wellKnown['org.matrix.msc2965.authentication']?.issuer ?? '';
- oidcSupported = !!clientManager.oidcIssuer;
- } catch (e: any) {
- // OIDC is not supported as no .well-known
- log.warn(e);
- }
+ deviceFlowSupported = false;
+ await oidcDiscovery();
+ oidcSupported = !!clientManager.oidcIssuer;
passwordSupported = (await getLoginFlows(clientManager.homeserverUrl)).flows.some(x => x.type === 'm.login.password');
@@ -91,12 +103,12 @@ limitations under the License.
log.warn(e);
oidcSupported = false;
if (!passwordSupported) {
- throw new Error(`Homeserver is not compatible with this Matrix client: ${e?.message ?? 'An error occured'}`);
+ throw new Error(`Homeserver is not compatible with this Matrix client: ${e?.message ?? 'An error occurred'}`);
}
}
}
} catch (e: any) {
- errorMessage = e?.message ?? 'An error occured';
+ errorMessage = e?.message ?? 'An error occurred';
}
loadedServerInfo = clientManager.homeserverUrl;
}
@@ -136,6 +148,166 @@ limitations under the License.
const debouncedHomeserver = debounce(() => clientManager.homeserverUrl = homeserverInput, 250);
+ function startQRScan() {
+ errorMessage = '';
+ scanQrResult = '';
+ scanQrChecksum = [];
+ scanQrCode = true;
+ dagPrettyUri = '';
+ oidcDeviceFlow = undefined;
+ }
+
+ function onRendezvousFailure() {
+ errorMessage = 'Rendezvous failed';
+ }
+
+ /**
+ *
+ * {"rendezvous":{
+ * "algorithm":"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
+ * "key":"VC7riVue45UUWUZaFYybC9TtnwB8H+Potv5kayAnRyQ",
+ * "transport":{"type":"org.matrix.msc3886.http.v1","uri":"https://rendezvous.lab.element.dev/9aec8d8e-9937-46a1-bfce-c06314b037c7"}
+ * },
+ * "intent":"login.reciprocate"}
+ */
+ async function onQRScanResult() {
+ errorMessage = '';
+ console.log(scanQrResult);
+ try {
+ const json = JSON.parse(scanQrResult);
+ if (
+ typeof json !== 'object' ||
+ typeof json.intent !== 'string' ||
+ typeof json.rendezvous !== 'object' ||
+ typeof json.rendezvous.algorithm !== 'string' ||
+ typeof json.rendezvous.transport !== 'object' ||
+ typeof json.rendezvous.transport.type !== 'string'
+ ) {
+ throw new Error('Invalid QR code');
+ }
+ if (json.intent !== 'login.reciprocate') {
+ throw new Error('The other device must be signed in');
+ }
+ if (json.rendezvous.algorithm !== 'org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256') {
+ throw new Error('Unsupported rendezvous algorithm');
+ }
+ if (json.rendezvous.transport.type !== 'org.matrix.msc3886.http.v1') {
+ throw new Error('Unsupported transport type');
+ }
+ if (typeof json.rendezvous.transport.uri !== 'string') {
+ throw new Error('Transport URI is missing');
+ }
+ if (typeof json.rendezvous.key !== 'string') {
+ throw new Error('Rendezvous key is missing');
+ }
+ try {
+ new URL(json.rendezvous.transport.uri);
+ } catch (e: any) {
+ throw new Error(`Invalid transport URI: ${e.message}`);
+ }
+ const code = JSON.parse(scanQrResult) as ECDHv2RendezvousCode;
+ const transport = new MSC3886SimpleHttpRendezvousTransport({ details: code.rendezvous.transport as MSC3886SimpleHttpRendezvousTransportDetails });
+
+ rzChannel = new MSC3903ECDHv2RendezvousChannel(
+ transport,
+ decodeBase64(code.rendezvous.key),
+ onRendezvousFailure,
+ );
+ scanQrChecksum = await rzChannel.connect();
+ const protocols = await rzChannel.receive();
+
+ if (!protocols?.protocols || !Array.isArray(protocols.protocols) || !protocols.homeserver) {
+ throw new Error('Received invalid protocols from other device');
+ }
+
+ if (!protocols.protocols.includes('org.matrix.msc3906.device_authorization_grant')) {
+ throw new Error('No supported login protocol');
+ }
+
+ homeserverInput = protocols.homeserver;
+ // check if we can connect to the homeserver
+ clientManager.homeserverUrl = protocols.homeserver;
+ await oidcDiscovery ();
+
+ if (!clientManager.oidcIssuer) {
+ throw new Error('Homeserver does not support OIDC');
+ }
+
+ oidcDeviceFlow = await clientManager.startLoginWithOidcDeviceFlow();
+ const { verification_uri, verification_uri_complete, user_code} = oidcDeviceFlow;
+
+ const url = new URL(verification_uri ?? verification_uri_complete ?? '');
+ dagPrettyUri = url.host + url.pathname;
+
+ await rzChannel.send({ type: PayloadType.Progress, protocol: "org.matrix.msc3906.device_authorization_grant", device_authorization_grant: {
+ verification_uri, verification_uri_complete, user_code,
+ } });
+
+ await clientManager.waitForLoginWithOidcDeviceFlow();
+
+ await rzChannel.send({ type: PayloadType.Finish, outcome: Outcome.Success, device_id: clientManager.deviceId, device_key: clientManager.client.getDeviceEd25519Key() });
+ const next = await rzChannel.receive();
+ console.log(next);
+ } catch (e: any) {
+ errorMessage = e.message;
+ await rzChannel?.close();
+ return;
+ }
+ // await aliceRz.generateCode();
+ // const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
+
+ // expect(code.rendezvous.key).toBeDefined();
+
+ // const aliceStartProm = aliceRz.startAfterShowingCode();
+
+ // // bob is try to sign in and scans the code
+ // const bobOnFailure = jest.fn();
+ // const bobEcdh = new MSC3903ECDHRendezvousChannel(
+ // bobTransport,
+ // decodeBase64(code.rendezvous.key), // alice's public key
+ // bobOnFailure,
+ // );
+
+ // const bobStartPromise = (async () => {
+ // const bobChecksum = await bobEcdh.connect();
+ // logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
+ // // await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
+
+ // // wait for protocols
+ // logger.info("Bob waiting for protocols");
+ // const protocols = await bobEcdh.receive();
+
+ // logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
+
+ // expect(protocols).toEqual({
+ // type: "m.login.progress",
+ // protocols: ["org.matrix.msc3906.login_token"],
+ // });
+
+ // await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
+ // })();
+
+ // await aliceStartProm;
+ // await bobStartPromise;
+
+ // const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
+
+ // const bobLoginProm = (async () => {
+ // const loginToken = await bobEcdh.receive();
+ // expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
+ // await bobEcdh.send({ type: "m.login.finish", outcome: "success", device_id: "BOB", device_key: "bbbb" });
+ // })();
+
+ // expect(await confirmProm).toEqual("BOB");
+ // await bobLoginProm;
+ }
+
+ async function dontMatch() {
+ await rzChannel?.cancel(RendezvousFailureReason.DataMismatch);
+ rzChannel = undefined;
+ scanQrCode = false;
+ }
+
onMount(async () => {
log.debug('onMount()');
if (params.has('error')) {
@@ -158,83 +330,126 @@ limitations under the License.