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 { 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/state/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?: any) => 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 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 ( Generate a Recovery Key for verifying identity if you do not have access to other devices. Additionally, setup a passphrase as a memorable alternative. Passphrase (Optional) {setupState.status === AsyncStatus.Error && ( {setupState.error ? setupState.error.message : 'Unexpected Error!'} )} {nextAuthData !== null && uiaAction && ( ( Authentication steps to perform this action are not supported by client. )} > {(ongoingFlow) => ( )} )} ); } type RecoveryKeyDisplayProps = { recoveryKey: string; }; function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) { 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 ( Store the Recovery Key in a safe place for future use, as you will need it to verify your identity if you do not have access to other devices. Recovery Key {safeToDisplayKey} setShow(!show)} variant="Secondary" radii="Pill"> {show ? 'Hide' : 'Show'} ); } type DeviceVerificationSetupProps = { onCancel: () => void; }; export const DeviceVerificationSetup = forwardRef( ({ onCancel }, ref) => { const [recoveryKey, setRecoveryKey] = useState(); return (
Setup Device Verification
{recoveryKey ? ( ) : ( )}
); } ); type DeviceVerificationResetProps = { onCancel: () => void; }; export const DeviceVerificationReset = forwardRef( ({ onCancel }, ref) => { const [reset, setReset] = useState(false); return (
Reset Device Verification
{reset ? ( {(recoveryKey: string | undefined, setRecoveryKey) => recoveryKey ? ( ) : ( ) } ) : ( ✋🧑‍🚒🤚 Resetting device verification is permanent. Anyone you have verified with will see security alerts and your encryption backup will be lost. You almost certainly do not want to do this, unless you have lost{' '} Recovery Key or Recovery Passphrase and every device you can verify from. )}
); } );