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.

{}}> - - - e.g. https://matrix.org - - {#if errorMessage} -

- {errorMessage} -

- {/if} - {#if oidcDeviceFlow} -
- {#if oidcDeviceFlow.verification_uri_complete} -

Scan this code on your other device to continue sign in

-
- -
+ {#if scanQrCode} + + {#if errorMessage} +

+ {errorMessage} +

+ {/if} + {#if scanQrResult} +

+ Check that the emojis below match those shown on your other device: +

+

+ {scanQrChecksum.join(' ')} +

+ {#if dagPrettyUri} +

+ Once confirmed, your other device will open {dagPrettyUri} to complete sign in. Confirm or enter code {oidcDeviceFlow?.user_code} as needed. +

{/if} -

{oidcDeviceFlow.verification_uri_complete ? 'or go' : 'Go'} to:

-

{oidcDeviceFlow.verification_uri}

-

and enter code:

-

{oidcDeviceFlow.user_code}

+ {#if errorMessage} + + {/if} + -

Changed your mind?

+ {:else} + errorMessage = 'Camera permission denied', + }} /> + + {/if} +
+ {:else} + + + e.g. https://matrix.org + + {#if errorMessage} +

+ {errorMessage} +

+ {/if} + + {#if oidcDeviceFlow} +
+ {#if oidcDeviceFlow.verification_uri_complete} +

Scan this code on your other device to continue sign in

+
+ +
+ {/if} +

{oidcDeviceFlow.verification_uri_complete ? 'or go' : 'Go'} to:

+

{oidcDeviceFlow.verification_uri}

+

and enter code:

+

{oidcDeviceFlow.user_code}

+ +

Changed your mind?

+ + +
+ {:else if oidcSupported} +

+ Homeserver { clientManager.homeserverUrl } supports auth via OIDC: +

+ {#if deviceFlowSupported } +

+ Already signed in on another device? You can use it to complete sign in +

+ +

or:

+ {/if} - -
- {:else if oidcSupported} -

- Homeserver { clientManager.homeserverUrl } supports auth via OIDC: -

- {#if deviceFlowSupported } + {:else if passwordSupported}

- Already signed in on another device? You can use it to complete sign in + Homeserver { clientManager.homeserverUrl } supports auth via Matrix password:

- -

or:

+ {/if} + {/if} - - {:else if passwordSupported}

- Homeserver { clientManager.homeserverUrl } supports auth via Matrix password: + Not got an account? +

- - Your matrix username - - - Your matrix password - - - {/if} -

- Not got an account? - -

-
+ + {/if}

It is recommended to use a dedicated test account as the E2E encryption settings used in this beta version may clash with your existing account and your keys get lost.

diff --git a/src/components/auth/ScanQRCode.svelte b/src/components/auth/ScanQRCode.svelte new file mode 100644 index 0000000..e355b00 --- /dev/null +++ b/src/components/auth/ScanQRCode.svelte @@ -0,0 +1,132 @@ + + + + +