import React, { FormEventHandler, forwardRef, useCallback, useState } from 'react'; import { Dialog, Header, Box, Text, IconButton, Icon, Icons, config, Button, Chip, color, Spinner, } from 'folds'; import FileSaver from 'file-saver'; import to from 'await-to-js'; import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk'; import { useTranslation } from 'react-i18next'; import { PasswordInput } from './password-input'; import { ContainerColor } from '../styles/ContainerColor.css'; import { copyToClipboard } from '../utils/dom'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { clearSecretStorageKeys } from '../../client/secretStorageKeys'; import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA'; import { useMatrixClient } from '../hooks/useMatrixClient'; import { useAlive } from '../hooks/useAlive'; import { UseStateProvider } from './UseStateProvider'; type UIACallback = ( authDict: AuthDict | null ) => Promise<[IAuthData, undefined] | [undefined, T]>; type PerformAction = (authDict: AuthDict | null) => Promise; type UIAAction = { authData: IAuthData; callback: UIACallback; cancelCallback: () => void; }; function makeUIAAction( authData: IAuthData, performAction: PerformAction, resolve: (data: T) => void, reject: (error?: unknown) => void ): UIAAction { const action: UIAAction = { authData, callback: async (authDict) => { const [error, data] = await to(performAction(authDict)); if (error instanceof MatrixError && error.httpStatus === 401) { return [error.data as IAuthData, undefined]; } if (error) { reject(error); throw error; } resolve(data); return [undefined, data]; }, cancelCallback: reject, }; return action; } type SetupVerificationProps = { onComplete: (recoveryKey: string) => void; }; function SetupVerification({ onComplete }: SetupVerificationProps) { const { t } = useTranslation(); const mx = useMatrixClient(); const alive = useAlive(); const [uiaAction, setUIAAction] = useState>(); const [nextAuthData, setNextAuthData] = useState(); // null means no next action. const handleAction = useCallback( async (authDict: AuthDict) => { if (!uiaAction) { throw new Error('Unexpected Error! UIA action is perform without data.'); } if (alive()) { setNextAuthData(null); } const [authData] = await uiaAction.callback(authDict); if (alive() && authData) { setNextAuthData(authData); } }, [uiaAction, alive] ); const resetUIA = useCallback(() => { if (!alive()) return; setUIAAction(undefined); setNextAuthData(undefined); }, [alive]); const authUploadDeviceSigningKeys: UIAuthCallback = useCallback( (makeRequest) => new Promise((resolve, reject) => { makeRequest(null) .then(() => { resolve(); resetUIA(); }) .catch((error) => { if (error instanceof MatrixError && error.httpStatus === 401) { const authData = error.data as IAuthData; const action = makeUIAAction( authData, makeRequest as PerformAction, resolve, (err) => { resetUIA(); reject(err); } ); if (alive()) { setUIAAction(action); } else { reject(new Error('Authentication failed! Failed to setup device verification.')); } return; } reject(error); }); }), [alive, resetUIA] ); const [setupState, setup] = useAsyncCallback( useCallback( async (passphrase) => { const crypto = mx.getCrypto(); if (!crypto) throw new Error('Unexpected Error! Crypto module not found!'); const recoveryKeyData = await crypto.createRecoveryKeyFromPassphrase(passphrase); if (!recoveryKeyData.encodedPrivateKey) { throw new Error('Unexpected Error! Failed to create recovery key.'); } clearSecretStorageKeys(); await crypto.bootstrapSecretStorage({ createSecretStorageKey: async () => recoveryKeyData, setupNewSecretStorage: true, }); await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys, setupNewCrossSigning: true, }); await crypto.resetKeyBackup(); onComplete(recoveryKeyData.encodedPrivateKey); }, [mx, onComplete, authUploadDeviceSigningKeys] ) ); const loading = setupState.status === AsyncStatus.Loading; const handleSubmit: FormEventHandler = (evt) => { evt.preventDefault(); if (loading) return; const target = evt.target as HTMLFormElement | undefined; const passphraseInput = target?.passphraseInput as HTMLInputElement | undefined; let passphrase: string | undefined; if (passphraseInput && passphraseInput.value.length > 0) { passphrase = passphraseInput.value; } setup(passphrase); }; return ( {t('Settings.verification_setup_desc')} {t('Settings.verification_passphrase_optional')} {setupState.status === AsyncStatus.Error && ( {setupState.error ? setupState.error.message : t('Settings.verification_error_generic')} )} {nextAuthData !== null && uiaAction && ( {t('Settings.verification_uia_unsupported')}} > {(ongoingFlow) => ( )} )} ); } type RecoveryKeyDisplayProps = { recoveryKey: string; }; function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) { const { t } = useTranslation(); const [show, setShow] = useState(false); const handleCopy = () => { copyToClipboard(recoveryKey); }; const handleDownload = () => { const blob = new Blob([recoveryKey], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); }; const safeToDisplayKey = show ? recoveryKey : recoveryKey.replace(/[^\s]/g, '*'); return ( {t('Settings.verification_recovery_key_store_desc')} {t('Settings.verification_recovery_key')} {safeToDisplayKey} setShow(!show)} variant="Secondary" radii="Pill"> {show ? t('Settings.verification_hide') : t('Settings.verification_show')} ); } type DeviceVerificationSetupProps = { onCancel: () => void; }; export const DeviceVerificationSetup = forwardRef( ({ onCancel }, ref) => { const { t } = useTranslation(); const [recoveryKey, setRecoveryKey] = useState(); return (
{t('Settings.verification_setup_title')}
{recoveryKey ? ( ) : ( )}
); } ); type DeviceVerificationResetProps = { onCancel: () => void; }; export const DeviceVerificationReset = forwardRef( ({ onCancel }, ref) => { const { t } = useTranslation(); const [reset, setReset] = useState(false); return (
{t('Settings.verification_reset_title')}
{reset ? ( {(recoveryKey: string | undefined, setRecoveryKey) => recoveryKey ? ( ) : ( ) } ) : ( ✋🧑‍🚒🤚 {t('Settings.verification_reset_permanent')} {t('Settings.verification_reset_warning')} )}
); } );