mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-26 23:23:08 +01:00
Some checks are pending
Deploy to Netlify (dev) / Deploy to Netlify (push) Waiting to run
* rework general settings * account settings - WIP * add missing key prop * add object url hook * extract wide modal styles * profile settings and image editor - WIP * add outline style to upload card * remove file param from bind upload atom hook * add compact variant to upload card * add compact upload card renderer * add option to update profile avatar * add option to change profile displayname * allow displayname change based on capabilities check * rearrange settings components into folders * add system notification settings * add initial page param in settings * convert account data hook to typescript * add push rule hook * add notification mode hook * add notification mode switcher component * add all messages notification settings options * add special messages notification settings * add keyword notifications * add ignored users section * improve ignore user list strings * add about settings * add access token option in about settings * add developer tools settings * add expand button to account data dev tool option * update folds * fix editable active element textarea check * do not close dialog when editable element in focus * add text area plugins * add text area intent handler hook * add newline intent mod in text area * add next line hotkey in text area intent hook * add syntax error position dom utility function * add account data editor * add button to send new account data in dev tools * improve custom emoji plugin * add more custom emojis hooks * add text util css * add word break in setting tile title and description * emojis and sticker user settings - WIP * view image packs from settings * emoji pack editing - WIP * add option to edit pack meta * change saved changes message * add image edit and delete controls * add option to upload pack images and apply changes * fix state event type when updating image pack * lazy load pack image tile img * hide upload image button when user can not edit pack * add option to add or remove global image packs * upgrade to rust crypto (#2168) * update matrix js sdk * remove dead code * use rust crypto * update setPowerLevel usage * fix types * fix deprecated isRoomEncrypted method uses * fix deprecated room.currentState uses * fix deprecated import/export room keys func * fix merge issues in image pack file * fix remaining issues in image pack file * start indexedDBStore * update package lock and vite-plugin-top-level-await * user session settings - WIP * add useAsync hook * add password stage uia * add uia flow matrix error hook * add UIA action component * add options to delete sessions * add sso uia stage * fix SSO stage complete error * encryption - WIP * update user settings encryption terminology * add default variant to password input * use password input in uia password stage * add options for local backup in user settings * remove typo in import local backup password input label * online backup - WIP * fix uia sso action * move access token settings from about to developer tools * merge encryption tab into sessions and rename it to devices * add device placeholder tile * add logout dialog * add logout button for current device * move other devices in component * render unverified device verification tile * add learn more section for current device verification * add device verification status badge * add info card component * add index file for password input component * add types for secret storage * add component to access secret storage key * manual verification - WIP * update matrix-js-sdk to v35 * add manual verification * use react query for device list * show unverified tab on sidebar * fix device list updates * add session key details to current device * render restore encryption backup * fix loading state of restore backup * fix unverified tab settings closes after verification * key backup tile - WIP * fix unverified tab badge * rename session key to device key in device tile * improve backup restore functionality * fix restore button enabled after layout reload during restoring backup * update backup info on status change * add backup disconnection failures * add device verification using sas * restore backup after verification * show option to logout on startup error screen * fix key backup hook update on decryption key cached * add option to enable device verification * add device verification reset dialog * add logout button in settings drawer * add encrypted message lost on logout * fix backup restore never finish with 0 keys * fix setup dialog hides when enabling device verification * show backup details in menu * update setup device verification body copy * replace deprecated method * fix displayname appear as mxid in settings * remove old refactored codes * fix types
375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
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<T> = (
|
||
authDict: AuthDict | null
|
||
) => Promise<[IAuthData, undefined] | [undefined, T]>;
|
||
|
||
type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
|
||
|
||
type UIAAction<T> = {
|
||
authData: IAuthData;
|
||
callback: UIACallback<T>;
|
||
cancelCallback: () => void;
|
||
};
|
||
|
||
function makeUIAAction<T>(
|
||
authData: IAuthData,
|
||
performAction: PerformAction<T>,
|
||
resolve: (data: T) => void,
|
||
reject: (error?: any) => void
|
||
): UIAAction<T> {
|
||
const action: UIAAction<T> = {
|
||
authData,
|
||
callback: async (authDict) => {
|
||
const [error, data] = await to<T, MatrixError | Error>(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<UIAAction<void>>();
|
||
const [nextAuthData, setNextAuthData] = useState<IAuthData | null>(); // 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<void> = useCallback(
|
||
(makeRequest) =>
|
||
new Promise<void>((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<void>,
|
||
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<void, Error, [string | undefined]>(
|
||
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<HTMLFormElement> = (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 (
|
||
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
|
||
<Text size="T300">
|
||
Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
|
||
devices. Additionally, setup a passphrase as a memorable alternative.
|
||
</Text>
|
||
<Box direction="Column" gap="100">
|
||
<Text size="L400">Passphrase (Optional)</Text>
|
||
<PasswordInput name="passphraseInput" size="400" readOnly={loading} />
|
||
</Box>
|
||
<Button
|
||
type="submit"
|
||
disabled={loading}
|
||
before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
|
||
>
|
||
<Text size="B400">Continue</Text>
|
||
</Button>
|
||
{setupState.status === AsyncStatus.Error && (
|
||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||
<b>{setupState.error ? setupState.error.message : 'Unexpected Error!'}</b>
|
||
</Text>
|
||
)}
|
||
{nextAuthData !== null && uiaAction && (
|
||
<ActionUIAFlowsLoader
|
||
authData={nextAuthData ?? uiaAction.authData}
|
||
unsupported={() => (
|
||
<Text size="T200">
|
||
Authentication steps to perform this action are not supported by client.
|
||
</Text>
|
||
)}
|
||
>
|
||
{(ongoingFlow) => (
|
||
<ActionUIA
|
||
authData={nextAuthData ?? uiaAction.authData}
|
||
ongoingFlow={ongoingFlow}
|
||
action={handleAction}
|
||
onCancel={uiaAction.cancelCallback}
|
||
/>
|
||
)}
|
||
</ActionUIAFlowsLoader>
|
||
)}
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<Box direction="Column" gap="400">
|
||
<Text size="T300">
|
||
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.
|
||
</Text>
|
||
<Box direction="Column" gap="100">
|
||
<Text size="L400">Recovery Key</Text>
|
||
<Box
|
||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
||
style={{
|
||
padding: config.space.S300,
|
||
borderRadius: config.radii.R400,
|
||
}}
|
||
alignItems="Center"
|
||
justifyContent="Center"
|
||
gap="400"
|
||
>
|
||
<Text style={{ fontFamily: 'monospace' }} size="T200" priority="300">
|
||
{safeToDisplayKey}
|
||
</Text>
|
||
<Chip onClick={() => setShow(!show)} variant="Secondary" radii="Pill">
|
||
<Text size="B300">{show ? 'Hide' : 'Show'}</Text>
|
||
</Chip>
|
||
</Box>
|
||
</Box>
|
||
<Box direction="Column" gap="200">
|
||
<Button onClick={handleCopy}>
|
||
<Text size="B400">Copy</Text>
|
||
</Button>
|
||
<Button onClick={handleDownload} fill="Soft">
|
||
<Text size="B400">Download</Text>
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
type DeviceVerificationSetupProps = {
|
||
onCancel: () => void;
|
||
};
|
||
export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
|
||
({ onCancel }, ref) => {
|
||
const [recoveryKey, setRecoveryKey] = useState<string>();
|
||
|
||
return (
|
||
<Dialog ref={ref}>
|
||
<Header
|
||
style={{
|
||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||
borderBottomWidth: config.borderWidth.B300,
|
||
}}
|
||
variant="Surface"
|
||
size="500"
|
||
>
|
||
<Box grow="Yes">
|
||
<Text size="H4">Setup Device Verification</Text>
|
||
</Box>
|
||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||
<Icon src={Icons.Cross} />
|
||
</IconButton>
|
||
</Header>
|
||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||
{recoveryKey ? (
|
||
<RecoveryKeyDisplay recoveryKey={recoveryKey} />
|
||
) : (
|
||
<SetupVerification onComplete={setRecoveryKey} />
|
||
)}
|
||
</Box>
|
||
</Dialog>
|
||
);
|
||
}
|
||
);
|
||
type DeviceVerificationResetProps = {
|
||
onCancel: () => void;
|
||
};
|
||
export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
|
||
({ onCancel }, ref) => {
|
||
const [reset, setReset] = useState(false);
|
||
|
||
return (
|
||
<Dialog ref={ref}>
|
||
<Header
|
||
style={{
|
||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||
borderBottomWidth: config.borderWidth.B300,
|
||
}}
|
||
variant="Surface"
|
||
size="500"
|
||
>
|
||
<Box grow="Yes">
|
||
<Text size="H4">Reset Device Verification</Text>
|
||
</Box>
|
||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||
<Icon src={Icons.Cross} />
|
||
</IconButton>
|
||
</Header>
|
||
{reset ? (
|
||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||
<UseStateProvider initial={undefined}>
|
||
{(recoveryKey: string | undefined, setRecoveryKey) =>
|
||
recoveryKey ? (
|
||
<RecoveryKeyDisplay recoveryKey={recoveryKey} />
|
||
) : (
|
||
<SetupVerification onComplete={setRecoveryKey} />
|
||
)
|
||
}
|
||
</UseStateProvider>
|
||
</Box>
|
||
) : (
|
||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||
<Box direction="Column" gap="200">
|
||
<Text size="H1">✋🧑🚒🤚</Text>
|
||
<Text size="T300">Resetting device verification is permanent.</Text>
|
||
<Text size="T300">
|
||
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{' '}
|
||
<b>Recovery Key</b> or <b>Recovery Passphrase</b> and every device you can verify
|
||
from.
|
||
</Text>
|
||
</Box>
|
||
<Button variant="Critical" onClick={() => setReset(true)}>
|
||
<Text size="B400">Reset</Text>
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
</Dialog>
|
||
);
|
||
}
|
||
);
|