mirror of
https://github.com/array-in-a-matrix/xinny.git
synced 2024-05-01 04:37:01 -04:00
224 lines
7.4 KiB
JavaScript
224 lines
7.4 KiB
JavaScript
/* eslint-disable react/jsx-one-expression-per-line */
|
||
import React, { useState } from 'react';
|
||
import './CrossSigning.scss';
|
||
import FileSaver from 'file-saver';
|
||
import { Formik } from 'formik';
|
||
import { twemojify } from '../../../util/twemojify';
|
||
|
||
import initMatrix from '../../../client/initMatrix';
|
||
import { openReusableDialog } from '../../../client/action/navigation';
|
||
import { copyToClipboard } from '../../../util/common';
|
||
import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys';
|
||
|
||
import Text from '../../atoms/text/Text';
|
||
import Button from '../../atoms/button/Button';
|
||
import Input from '../../atoms/input/Input';
|
||
import Spinner from '../../atoms/spinner/Spinner';
|
||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||
|
||
import { authRequest } from './AuthRequest';
|
||
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
||
|
||
const failedDialog = () => {
|
||
const renderFailure = (requestClose) => (
|
||
<div className="cross-signing__failure">
|
||
<Text variant="h1">{twemojify('❌')}</Text>
|
||
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
|
||
<Button onClick={requestClose}>Close</Button>
|
||
</div>
|
||
);
|
||
|
||
openReusableDialog(
|
||
<Text variant="s1" weight="medium">Setup cross signing</Text>,
|
||
renderFailure,
|
||
);
|
||
};
|
||
|
||
const securityKeyDialog = (key) => {
|
||
const downloadKey = () => {
|
||
const blob = new Blob([key.encodedPrivateKey], {
|
||
type: 'text/plain;charset=us-ascii',
|
||
});
|
||
FileSaver.saveAs(blob, 'security-key.txt');
|
||
};
|
||
const copyKey = () => {
|
||
copyToClipboard(key.encodedPrivateKey);
|
||
};
|
||
|
||
const renderSecurityKey = () => (
|
||
<div className="cross-signing__key">
|
||
<Text weight="medium">Please save this security key somewhere safe.</Text>
|
||
<Text className="cross-signing__key-text">
|
||
{key.encodedPrivateKey}
|
||
</Text>
|
||
<div className="cross-signing__key-btn">
|
||
<Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
|
||
<Button onClick={() => downloadKey(key)}>Download</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// Download automatically.
|
||
downloadKey();
|
||
|
||
openReusableDialog(
|
||
<Text variant="s1" weight="medium">Security Key</Text>,
|
||
() => renderSecurityKey(),
|
||
);
|
||
};
|
||
|
||
function CrossSigningSetup() {
|
||
const initialValues = { phrase: '', confirmPhrase: '' };
|
||
const [genWithPhrase, setGenWithPhrase] = useState(undefined);
|
||
|
||
const setup = async (securityPhrase = undefined) => {
|
||
const mx = initMatrix.matrixClient;
|
||
setGenWithPhrase(typeof securityPhrase === 'string');
|
||
const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
|
||
clearSecretStorageKeys();
|
||
|
||
await mx.bootstrapSecretStorage({
|
||
createSecretStorageKey: async () => recoveryKey,
|
||
setupNewKeyBackup: true,
|
||
setupNewSecretStorage: true,
|
||
});
|
||
|
||
const authUploadDeviceSigningKeys = async (makeRequest) => {
|
||
const isDone = await authRequest('Setup cross signing', async (auth) => {
|
||
await makeRequest(auth);
|
||
});
|
||
setTimeout(() => {
|
||
if (isDone) securityKeyDialog(recoveryKey);
|
||
else failedDialog();
|
||
});
|
||
};
|
||
|
||
await mx.bootstrapCrossSigning({
|
||
authUploadDeviceSigningKeys,
|
||
setupNewCrossSigning: true,
|
||
});
|
||
};
|
||
|
||
const validator = (values) => {
|
||
const errors = {};
|
||
if (values.phrase === '12345678') {
|
||
errors.phrase = 'How about 87654321 ?';
|
||
}
|
||
if (values.phrase === '87654321') {
|
||
errors.phrase = 'Your are playing with 🔥';
|
||
}
|
||
const PHRASE_REGEX = /^([^\s]){8,127}$/;
|
||
if (values.phrase.length > 0 && !PHRASE_REGEX.test(values.phrase)) {
|
||
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
|
||
}
|
||
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
|
||
errors.confirmPhrase = 'Phrase don\'t match.';
|
||
}
|
||
return errors;
|
||
};
|
||
|
||
return (
|
||
<div className="cross-signing__setup">
|
||
<div className="cross-signing__setup-entry">
|
||
<Text>
|
||
We will generate a <b>Security Key</b>,
|
||
which you can use to manage messages backup and session verification.
|
||
</Text>
|
||
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
|
||
{genWithPhrase === false && <Spinner size="small" />}
|
||
</div>
|
||
<Text className="cross-signing__setup-divider">OR</Text>
|
||
<Formik
|
||
initialValues={initialValues}
|
||
onSubmit={(values) => setup(values.phrase)}
|
||
validate={validator}
|
||
>
|
||
{({
|
||
values, errors, handleChange, handleSubmit,
|
||
}) => (
|
||
<form
|
||
className="cross-signing__setup-entry"
|
||
onSubmit={handleSubmit}
|
||
disabled={genWithPhrase !== undefined}
|
||
>
|
||
<Text>
|
||
Alternatively you can also set a <b>Security Phrase </b>
|
||
so you don't have to remember long Security Key,
|
||
and optionally save the Key as backup.
|
||
</Text>
|
||
<Input
|
||
name="phrase"
|
||
value={values.phrase}
|
||
onChange={handleChange}
|
||
label="Security Phrase"
|
||
type="password"
|
||
required
|
||
disabled={genWithPhrase !== undefined}
|
||
/>
|
||
{errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
|
||
<Input
|
||
name="confirmPhrase"
|
||
value={values.confirmPhrase}
|
||
onChange={handleChange}
|
||
label="Confirm Security Phrase"
|
||
type="password"
|
||
required
|
||
disabled={genWithPhrase !== undefined}
|
||
/>
|
||
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
|
||
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
|
||
{genWithPhrase === true && <Spinner size="small" />}
|
||
</form>
|
||
)}
|
||
</Formik>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const setupDialog = () => {
|
||
openReusableDialog(
|
||
<Text variant="s1" weight="medium">Setup cross signing</Text>,
|
||
() => <CrossSigningSetup />,
|
||
);
|
||
};
|
||
|
||
function CrossSigningReset() {
|
||
return (
|
||
<div className="cross-signing__reset">
|
||
<Text variant="h1">{twemojify('✋🧑🚒🤚')}</Text>
|
||
<Text weight="medium">Resetting cross-signing keys is permanent.</Text>
|
||
<Text>
|
||
Anyone you have verified with will see security alerts and your message backup will be lost.
|
||
You almost certainly do not want to do this,
|
||
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
|
||
every session you can cross-sign from.
|
||
</Text>
|
||
<Button variant="danger" onClick={setupDialog}>Reset</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const resetDialog = () => {
|
||
openReusableDialog(
|
||
<Text variant="s1" weight="medium">Reset cross signing</Text>,
|
||
() => <CrossSigningReset />,
|
||
);
|
||
};
|
||
|
||
function CrossSignin() {
|
||
const isCSEnabled = useCrossSigningStatus();
|
||
return (
|
||
<SettingTile
|
||
title="Cross signing"
|
||
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
|
||
options={(
|
||
isCSEnabled
|
||
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
|
||
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
|
||
)}
|
||
/>
|
||
);
|
||
}
|
||
|
||
export default CrossSignin;
|