2022-04-24 15:42:24 +05:30
|
|
|
|
/* 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>
|
2023-03-30 16:42:33 +02:00
|
|
|
|
Anyone you have verified with will see security alerts and your message backup will be lost.
|
2022-04-24 15:42:24 +05:30
|
|
|
|
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;
|