From 5784a005dc9f3135c041c4fffa816d3609ff9bc2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2022 14:34:15 +0100 Subject: [PATCH 1/9] Auto-register if displayName URL param is given Fixes https://github.com/vector-im/element-call/issues/442 --- src/auth/generateRandomName.ts | 4 +--- src/room/RoomAuthView.jsx | 35 +++++++--------------------------- src/room/RoomPage.jsx | 27 +++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/auth/generateRandomName.ts b/src/auth/generateRandomName.ts index b58c1114..2232d356 100644 --- a/src/auth/generateRandomName.ts +++ b/src/auth/generateRandomName.ts @@ -19,7 +19,6 @@ import { adjectives, colors, animals, - Config, } from "unique-names-generator"; const elements = [ @@ -143,12 +142,11 @@ const elements = [ "oganesson", ]; -export function generateRandomName(config: Config): string { +export function generateRandomName(): string { return uniqueNamesGenerator({ dictionaries: [colors, adjectives, animals, elements], style: "lowerCase", length: 3, separator: "-", - ...config, }); } diff --git a/src/room/RoomAuthView.jsx b/src/room/RoomAuthView.jsx index 4bf2303a..05613146 100644 --- a/src/room/RoomAuthView.jsx +++ b/src/room/RoomAuthView.jsx @@ -16,26 +16,21 @@ limitations under the License. import React, { useCallback, useState } from "react"; import styles from "./RoomAuthView.module.css"; -import { useClient } from "../ClientContext"; import { Button } from "../button"; import { Body, Caption, Link, Headline } from "../typography/Typography"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { useLocation } from "react-router-dom"; -import { useRecaptcha } from "../auth/useRecaptcha"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; -import { randomString } from "matrix-js-sdk/src/randomstring"; -import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; import { Form } from "../form/Form"; import { UserMenuContainer } from "../UserMenuContainer"; -import { generateRandomName } from "../auth/generateRandomName"; +import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; export function RoomAuthView() { - const { setClient } = useClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); - const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); + + const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } = + useRegisterPasswordlessUser(); const onSubmit = useCallback( (e) => { @@ -43,29 +38,13 @@ export function RoomAuthView() { const data = new FormData(e.target); const displayName = data.get("displayName"); - async function submit() { - setError(undefined); - setLoading(true); - const recaptchaResponse = await execute(); - const userName = generateRandomName(); - const [client, session] = await register( - userName, - randomString(16), - displayName, - recaptchaResponse, - true - ); - setClient(client, session); - } - - submit().catch((error) => { - console.error(error); + registerPasswordlessUser(displayName).catch((error) => { + console.error("Failed to register passwordless user", e); setLoading(false); setError(error); - reset(); }); }, - [register, reset, execute] + [registerPasswordlessUser] ); const location = useLocation(); diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.jsx index 1fe7eea6..72f6bf44 100644 --- a/src/room/RoomPage.jsx +++ b/src/room/RoomPage.jsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; import { useClient } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; @@ -22,6 +22,7 @@ import { RoomAuthView } from "./RoomAuthView"; import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallView } from "./GroupCallView"; import { MediaHandlerProvider } from "../settings/useMediaHandler"; +import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; export function RoomPage() { const { loading, isAuthenticated, error, client, isPasswordlessUser } = @@ -29,17 +30,37 @@ export function RoomPage() { const { roomId: maybeRoomId } = useParams(); const { hash, search } = useLocation(); - const [viaServers, isEmbedded, isPtt] = useMemo(() => { + const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { const params = new URLSearchParams(search); return [ params.getAll("via"), params.has("embed"), params.get("ptt") === "true", + params.get("displayName"), ]; }, [search]); const roomId = (maybeRoomId || hash || "").toLowerCase(); + const { registerPasswordlessUser, recaptchaId } = + useRegisterPasswordlessUser(); + const [isRegistering, setIsRegistering] = useState(false); - if (loading) { + useEffect(() => { + // If we're not already authed and we've been given a display name as + // a URL param, automatically register a passwordless user + if (!isAuthenticated && displayName) { + setIsRegistering(true); + registerPasswordlessUser(displayName).finally(() => { + setIsRegistering(false); + }); + } + }, [ + isAuthenticated, + displayName, + setIsRegistering, + registerPasswordlessUser, + ]); + + if (loading || isRegistering) { return ; } From c1e45c4a303db294470603add7e95ef7f7cb0d97 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2022 16:02:17 +0100 Subject: [PATCH 2/9] Missed a file --- src/auth/useRegisterPasswordlessUser.ts | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/auth/useRegisterPasswordlessUser.ts diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts new file mode 100644 index 00000000..2fd74579 --- /dev/null +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -0,0 +1,59 @@ +/* +Copyright 2022 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 { useCallback } from "react"; +import { randomString } from "matrix-js-sdk/src/randomstring"; + +import { useClient } from "../ClientContext"; +import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; +import { generateRandomName } from "../auth/generateRandomName"; +import { useRecaptcha } from "../auth/useRecaptcha"; + +export interface UseRegisterPasswordlessUserType { + privacyPolicyUrl: string; + registerPasswordlessUser: (string) => Promise; + recaptchaId: string; +} + +export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { + const { setClient } = useClient(); + const [privacyPolicyUrl, recaptchaKey, register] = + useInteractiveRegistration(); + const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); + + const registerPasswordlessUser = useCallback( + async (displayName: string) => { + try { + const recaptchaResponse = await execute(); + const userName = generateRandomName(); + const [client, session] = await register( + userName, + randomString(16), + displayName, + recaptchaResponse, + true + ); + setClient(client, session); + } catch (e) { + reset(); + throw e; + } + }, + [execute, reset, register, setClient] + ); + + return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId }; +} From 4c145af7a3bc0a67f4201a57d16a1a787796539d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:07:30 +0100 Subject: [PATCH 3/9] Don't restore session unless crypto data is found Add a check to ensure that we find crypto data in the crypto store when we're restoring a session and otherwise abort the session restore. This will prevent us from restoring a session and generating new keys when there was a previous session with different keys. ***This will force a logout for all users*** See the linked issue (and the comment in code) for more detail. Fixes https://github.com/vector-im/element-call/issues/464 --- src/ClientContext.tsx | 15 +++++--- src/auth/useInteractiveLogin.ts | 15 +++++--- src/auth/useInteractiveRegistration.ts | 15 +++++--- src/matrix-utils.ts | 51 ++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 68676ad5..503c3f8f 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -101,12 +101,15 @@ export const ClientProvider: FC = ({ children }) => { const { user_id, device_id, access_token, passwordlessUser } = session; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + true + ); /* eslint-enable camelcase */ return { client, isPasswordlessUser: passwordlessUser }; diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 6379c67c..9ffa7ba6 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -57,12 +57,15 @@ export const useInteractiveLogin = () => passwordlessUser: false, }; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + false + ); /* eslint-enable camelcase */ return [client, session]; diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index af611f9d..8b8647a0 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -90,12 +90,15 @@ export const useInteractiveRegistration = (): [ const { user_id, access_token, device_id } = (await interactiveAuth.attemptAuth()) as any; - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const client = await initClient( + { + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }, + false + ); await client.setDisplayName(displayName); diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 1a1a2ea6..ea2920b0 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -24,6 +24,19 @@ export const defaultHomeserver = export const defaultHomeserverHost = new URL(defaultHomeserver).host; +export class CryptoStoreIntegrityError extends Error { + constructor() { + super("Crypto store data was expected, but none was found"); + } +} + +const SYNC_STORE_NAME = "element-call-sync"; +// Note that the crypto store name has changed from previous versions +// deliberately in order to force a logout for all users due to +// https://github.com/vector-im/element-call/issues/464 +// (It's a good opportunity to make the database names consistent.) +const CRYPTO_STORE_NAME = "element-call-crypto"; + function waitForSync(client: MatrixClient) { return new Promise((resolve, reject) => { const onSync = ( @@ -43,8 +56,18 @@ function waitForSync(client: MatrixClient) { }); } +/** + * Initialises and returns a new Matrix Client + * If true is passed for the 'restore' parameter, a check will be made + * to ensure that corresponding crypto data is stored and recovered. + * If the check fails, CryptoStoreIntegrityError will be thrown. + * @param clientOptions Object of options passed through to the client + * @param restore Whether the session is being restored from storage + * @returns The MatrixClient instance + */ export async function initClient( - clientOptions: ICreateClientOpts + clientOptions: ICreateClientOpts, + restore: boolean ): Promise { // TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10 window.OLM_OPTIONS = {}; @@ -62,17 +85,39 @@ export async function initClient( storeOpts.store = new IndexedDBStore({ indexedDB: window.indexedDB, localStorage, - dbName: "element-call-sync", + dbName: SYNC_STORE_NAME, workerFactory: () => new IndexedDBWorker(), }); } else if (localStorage) { storeOpts.store = new MemoryStore({ localStorage }); } + // Check whether we have crypto data store. If we are restoring a session + // from storage then we will have started the crypto store and therefore + // have generated keys for that device, so if we can't recover those keys, + // we must not continue or we'll generate new keys and anyone who saw our + // previous keys will not accept our new key. + if (restore) { + if (indexedDB) { + const cryptoStoreExists = await IndexedDBCryptoStore.exists( + indexedDB, + CRYPTO_STORE_NAME + ); + if (!cryptoStoreExists) throw new CryptoStoreIntegrityError(); + } else if (localStorage) { + if (!LocalStorageCryptoStore.exists(localStorage)) + throw new CryptoStoreIntegrityError(); + } else { + // if we get here then we're using the memory store, which cannot + // possibly have remembered a session, so it's an error. + throw new CryptoStoreIntegrityError(); + } + } + if (indexedDB) { storeOpts.cryptoStore = new IndexedDBCryptoStore( indexedDB, - "matrix-js-sdk:crypto" + CRYPTO_STORE_NAME ); } else if (localStorage) { storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); From 1eab957d85080332350da730aeceb3f526373c4b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:11:47 +0100 Subject: [PATCH 4/9] Fix typescript syntax --- src/auth/useRegisterPasswordlessUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts index 2fd74579..73189471 100644 --- a/src/auth/useRegisterPasswordlessUser.ts +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -24,7 +24,7 @@ import { useRecaptcha } from "../auth/useRecaptcha"; export interface UseRegisterPasswordlessUserType { privacyPolicyUrl: string; - registerPasswordlessUser: (string) => Promise; + registerPasswordlessUser: (displayName: string) => Promise; recaptchaId: string; } From 873e68e1e17db6f3bcb19fbb26976bf45eff9bb8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2022 13:24:22 +0100 Subject: [PATCH 5/9] Add notes from thinking through the need for storing what crypto db we use --- src/matrix-utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index ea2920b0..3e576e78 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -97,6 +97,12 @@ export async function initClient( // have generated keys for that device, so if we can't recover those keys, // we must not continue or we'll generate new keys and anyone who saw our // previous keys will not accept our new key. + // It's worth mentioning here that if support for indexeddb or localstorage + // appears or disappears between sessions (it happens) then the failure mode + // here will be that we'll try a different store, not find crypto data and + // fail to restore the session. An alternative would be to continue using + // whatever we were using before, but that could be confusing since you could + // enable indexeddb and but the app would still not be using it. if (restore) { if (indexedDB) { const cryptoStoreExists = await IndexedDBCryptoStore.exists( From d01f7be58a269d3b2a0d733059eba8c63b09c078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:28:16 +0200 Subject: [PATCH 6/9] Add `.env.example` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .env.example | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..2f62d615 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +#### +# App Config +# Environment files are documented here: +# https://vitejs.dev/guide/env-and-mode.html#env-files +#### + +# Used for determining the homeserver to use for short urls etc. +# VITE_DEFAULT_HOMESERVER=http://localhost:8008 + +# Used for submitting debug logs to an external rageshake server +# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit + +# The Sentry DSN to use for error reporting. Leave undefined to disable. +# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 + +# VITE_CUSTOM_THEME=true +# VITE_THEME_ACCENT=#0dbd8b +# VITE_THEME_ACCENT_20=#0dbd8b33 +# VITE_THEME_ALERT=#ff5b55 +# VITE_THEME_ALERT_20=#ff5b5533 +# VITE_THEME_LINKS=#0086e6 +# VITE_THEME_PRIMARY_CONTENT=#ffffff +# VITE_THEME_SECONDARY_CONTENT=#a9b2bc +# VITE_THEME_TERTIARY_CONTENT=#8e99a4 +# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433 +# VITE_THEME_QUATERNARY_CONTENT=#6f7882 +# VITE_THEME_QUINARY_CONTENT=#394049 +# VITE_THEME_SYSTEM=#21262c +# VITE_THEME_BACKGROUND=#15191e From d097223d41aad16e55cb989ebc0c1d2e9227285c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:29:25 +0200 Subject: [PATCH 7/9] Add `.env` to `.gitignore` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a87b455..9bafa524 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .DS_Store +.env dist dist-ssr *.local From f876df6acce6f1243f2a037de4346c03c30d6318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:30:52 +0200 Subject: [PATCH 8/9] Remove `.env` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .env | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 2f62d615..00000000 --- a/.env +++ /dev/null @@ -1,29 +0,0 @@ -#### -# App Config -# Environment files are documented here: -# https://vitejs.dev/guide/env-and-mode.html#env-files -#### - -# Used for determining the homeserver to use for short urls etc. -# VITE_DEFAULT_HOMESERVER=http://localhost:8008 - -# Used for submitting debug logs to an external rageshake server -# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit - -# The Sentry DSN to use for error reporting. Leave undefined to disable. -# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 - -# VITE_CUSTOM_THEME=true -# VITE_THEME_ACCENT=#0dbd8b -# VITE_THEME_ACCENT_20=#0dbd8b33 -# VITE_THEME_ALERT=#ff5b55 -# VITE_THEME_ALERT_20=#ff5b5533 -# VITE_THEME_LINKS=#0086e6 -# VITE_THEME_PRIMARY_CONTENT=#ffffff -# VITE_THEME_SECONDARY_CONTENT=#a9b2bc -# VITE_THEME_TERTIARY_CONTENT=#8e99a4 -# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433 -# VITE_THEME_QUATERNARY_CONTENT=#6f7882 -# VITE_THEME_QUINARY_CONTENT=#394049 -# VITE_THEME_SYSTEM=#21262c -# VITE_THEME_BACKGROUND=#15191e From bb505273f4f7fd0ae510619ae578690d2b2ce99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 15 Jul 2022 11:32:07 +0200 Subject: [PATCH 9/9] Add `.env` instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 75c9a5e6..076cb2c5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ git clone https://github.com/vector-im/element-call.git cd element-call yarn yarn link matrix-js-sdk +cp .env.example .env yarn dev ```