Add SSSS and key backup

This commit is contained in:
Ajay Bura 2022-04-24 13:17:36 +05:30
parent 89f69ed5b4
commit 101134e313
7 changed files with 188 additions and 62 deletions

View file

@ -0,0 +1,117 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './AuthRequest.scss';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
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 { useStore } from '../../hooks/useStore';
let lastUsedPassword;
const getAuthId = (password) => ({
type: 'm.login.password',
password,
identifier: {
type: 'm.id.user',
user: initMatrix.matrixClient.getUserId(),
},
});
function AuthRequest({ onComplete, makeRequest }) {
const [status, setStatus] = useState(false);
const mountStore = useStore();
const handleForm = async (e) => {
mountStore.setItem(true);
e.preventDefault();
const password = e.target.password.value;
if (password.trim() === '') return;
try {
setStatus({ ongoing: true });
await makeRequest(getAuthId(password));
lastUsedPassword = password;
if (!mountStore.getItem()) return;
onComplete(true);
} catch (err) {
lastUsedPassword = undefined;
if (!mountStore.getItem()) return;
if (err.errcode === 'M_FORBIDDEN') {
setStatus({ error: 'Wrong password. Please enter correct password.' });
return;
}
setStatus({ error: 'Request failed!' });
}
};
const handleChange = () => {
setStatus(false);
};
return (
<div className="auth-request">
<form onSubmit={handleForm}>
<Input
name="password"
label="Account password"
type="password"
onChange={handleChange}
required
/>
{status.ongoing && <Spinner size="small" />}
{status.error && <Text variant="b3">{status.error}</Text>}
{(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>Continue</Button>}
</form>
</div>
);
}
AuthRequest.propTypes = {
onComplete: PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
};
/**
* @param {string} title Title of dialog
* @param {(auth) => void} makeRequest request to make
* @returns {Promise<boolean>} whether the request succeed or not.
*/
export const authRequest = async (title, makeRequest) => {
try {
const auth = lastUsedPassword ? getAuthId(lastUsedPassword) : undefined;
await makeRequest(auth);
return true;
} catch (e) {
lastUsedPassword = undefined;
if (e.httpStatus !== 401 || e.data?.flows === undefined) return false;
const { flows } = e.data;
const canUsePassword = flows.find((f) => f.stages.includes('m.login.password'));
if (!canUsePassword) return false;
return new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<AuthRequest
onComplete={(done) => {
isCompleted = true;
resolve(done);
requestClose();
}}
makeRequest={makeRequest}
/>
),
() => {
if (!isCompleted) resolve(false);
},
);
});
}
};
export default AuthRequest;

View file

@ -0,0 +1,12 @@
.auth-request {
padding: var(--sp-normal);
& form > *:not(:first-child) {
margin-top: var(--sp-normal);
}
& .text-b3 {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
}

View file

@ -1,3 +1,4 @@
/* eslint-disable react/jsx-one-expression-per-line */
import React, { useState } from 'react'; import React, { useState } from 'react';
import './CrossSigning.scss'; import './CrossSigning.scss';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
@ -6,7 +7,6 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation'; import { openReusableDialog } from '../../../client/action/navigation';
import { hasCrossSigningAccountData } from '../../../util/matrixUtil';
import { copyToClipboard } from '../../../util/common'; import { copyToClipboard } from '../../../util/common';
import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys'; import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys';
@ -16,6 +16,24 @@ import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner'; import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile'; 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 securityKeyDialog = (key) => {
const downloadKey = () => { const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], { const blob = new Blob([key.encodedPrivateKey], {
@ -66,28 +84,19 @@ function CrossSigningSetup() {
}); });
const authUploadDeviceSigningKeys = async (makeRequest) => { const authUploadDeviceSigningKeys = async (makeRequest) => {
try { const isDone = await authRequest('Setup cross signing', async (auth) => {
const password = prompt('Password', ''); await makeRequest(auth);
await makeRequest({ });
type: 'm.login.password', setTimeout(() => {
password, if (isDone) securityKeyDialog(recoveryKey);
identifier: { else failedDialog();
type: 'm.id.user',
user: mx.getUserId(),
},
}); });
} catch (e) {
console.log(e);
// TODO: handle error
}
}; };
await mx.bootstrapCrossSigning({ await mx.bootstrapCrossSigning({
authUploadDeviceSigningKeys, authUploadDeviceSigningKeys,
setupNewCrossSigning: true, setupNewCrossSigning: true,
}); });
securityKeyDialog(recoveryKey);
}; };
const validator = (values) => { const validator = (values) => {
@ -112,7 +121,7 @@ function CrossSigningSetup() {
<div className="cross-signing__setup"> <div className="cross-signing__setup">
<div className="cross-signing__setup-entry"> <div className="cross-signing__setup-entry">
<Text> <Text>
We will generate a Security Key, We will generate a <b>Security Key</b>,
which you can use to manage messages backup and session verification. which you can use to manage messages backup and session verification.
</Text> </Text>
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>} {genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
@ -133,7 +142,7 @@ function CrossSigningSetup() {
disabled={genWithPhrase !== undefined} disabled={genWithPhrase !== undefined}
> >
<Text> <Text>
Alternatively you can also set a Security Phrase Alternatively you can also set a <b>Security Phrase </b>
so you don't have to remember long Security Key, so you don't have to remember long Security Key,
and optionally save the Key as backup. and optionally save the Key as backup.
</Text> </Text>
@ -181,7 +190,8 @@ function CrossSigningReset() {
<Text> <Text>
Anyone you have verified with will see security alerts and your message backup will lost. Anyone you have verified with will see security alerts and your message backup will lost.
You almost certainly do not want to do this, You almost certainly do not want to do this,
unless you have lost Security Key or Phrase and every session you can cross-sign from. unless you have lost <b>Security Key</b> or <b>Phrase</b> and
every session you can cross-sign from.
</Text> </Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button> <Button variant="danger" onClick={setupDialog}>Reset</Button>
</div> </div>
@ -196,12 +206,13 @@ const resetDialog = () => {
}; };
function CrossSignin() { function CrossSignin() {
const isCSEnabled = useCrossSigningStatus();
return ( return (
<SettingTile <SettingTile
title="Cross signing" title="Cross signing"
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>} content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
options={( options={(
hasCrossSigningAccountData() isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button> ? <Button variant="danger" onClick={resetDialog}>Reset</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</Button> : <Button variant="primary" onClick={setupDialog}>Setup</Button>
)} )}

View file

@ -45,10 +45,11 @@
} }
} }
.cross-signing__failure,
.cross-signing__reset { .cross-signing__reset {
padding: var(--sp-normal); padding: var(--sp-normal);
padding-top: var(--sp-extra-loose); padding-top: var(--sp-extra-loose);
& > .text { & > .text {
padding-bottom: var(--sp-loose); padding-bottom: var(--sp-normal);
} }
} }

View file

@ -17,6 +17,8 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import { authRequest } from './AuthRequest';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList'; import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
@ -71,39 +73,16 @@ function DeviceManage() {
} }
}; };
const handleRemove = async (device, auth = undefined) => { const handleRemove = async (device) => {
if (auth === undefined if (window.confirm(`You are about to logout "${device.display_name}" session.`)) {
? window.confirm(`You are about to logout "${device.display_name}" session.`)
: true
) {
addToProcessing(device); addToProcessing(device);
try { await authRequest(`Logout "${device.display_name}"`, async (auth) => {
await mx.deleteDevice(device.device_id, auth); await mx.deleteDevice(device.device_id, auth);
} catch (e) {
if (e.httpStatus === 401 && e.data?.flows) {
const { flows } = e.data;
const flow = flows.find((f) => f.stages.includes('m.login.password'));
if (flow) {
const password = window.prompt('Please enter account password', '');
if (password && password.trim() !== '') {
handleRemove(device, {
session: e.data.session,
type: 'm.login.password',
password,
identifier: {
type: 'm.id.user',
user: mx.getUserId(),
},
}); });
return;
}
}
}
window.alert('Failed to remove session!');
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
removeFromProcessing(device); removeFromProcessing(device);
} }
}
}; };
const renderDevice = (device, isVerified) => { const renderDevice = (device, isVerified) => {

View file

@ -82,7 +82,7 @@ CreateKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired, keyData: PropTypes.shape({}).isRequired,
}; };
function RestoreKeyBackupDialog({ keyData, backupInfo }) { function RestoreKeyBackupDialog({ keyData }) {
const [status, setStatus] = useState(false); const [status, setStatus] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
@ -103,6 +103,7 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) {
}; };
try { try {
const backupInfo = await mx.getKeyBackupVersion();
const info = await mx.restoreKeyBackupWithSecretStorage( const info = await mx.restoreKeyBackupWithSecretStorage(
backupInfo, backupInfo,
undefined, undefined,
@ -115,7 +116,7 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') { if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
deletePrivateKey(keyData.keyId); deletePrivateKey(keyData.keyId);
setStatus({ error: 'Failed to restore backup. Key is invalid', errorCode: 'BAD_KEY' }); setStatus({ error: 'Failed to restore backup. Key is invalid!', errorCode: 'BAD_KEY' });
} else { } else {
setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' }); setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
} }
@ -152,10 +153,9 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) {
} }
RestoreKeyBackupDialog.propTypes = { RestoreKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired, keyData: PropTypes.shape({}).isRequired,
backupInfo: PropTypes.shape({}).isRequired,
}; };
function DeleteKeyBackupDialog({ version, requestClose }) { function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
@ -164,7 +164,8 @@ function DeleteKeyBackupDialog({ version, requestClose }) {
const deleteBackup = async () => { const deleteBackup = async () => {
setIsDeleting(true); setIsDeleting(true);
try { try {
await mx.deleteKeyBackupVersion(version); const backupInfo = await mx.getKeyBackupVersion();
if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version);
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
requestClose(true); requestClose(true);
} catch { } catch {
@ -187,7 +188,6 @@ function DeleteKeyBackupDialog({ version, requestClose }) {
); );
} }
DeleteKeyBackupDialog.propTypes = { DeleteKeyBackupDialog.propTypes = {
version: PropTypes.string.isRequired,
requestClose: PropTypes.func.isRequired, requestClose: PropTypes.func.isRequired,
}; };
@ -217,7 +217,7 @@ function KeyBackup() {
return () => { return () => {
mx.removeListener('accountData', handleAccountData); mx.removeListener('accountData', handleAccountData);
}; };
}, []); }, [isCSEnabled]);
const openCreateKeyBackup = async () => { const openCreateKeyBackup = async () => {
const keyData = await accessSecretStorage('Create Key Backup'); const keyData = await accessSecretStorage('Create Key Backup');
@ -236,7 +236,7 @@ function KeyBackup() {
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>, <Text variant="s1" weight="medium">Restore Key Backup</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} backupInfo={keyBackup} />, () => <RestoreKeyBackupDialog keyData={keyData} />,
); );
}; };
@ -244,7 +244,6 @@ function KeyBackup() {
<Text variant="s1" weight="medium">Delete Key Backup</Text>, <Text variant="s1" weight="medium">Delete Key Backup</Text>,
(requestClose) => ( (requestClose) => (
<DeleteKeyBackupDialog <DeleteKeyBackupDialog
version={keyBackup.version}
requestClose={(isDone) => { requestClose={(isDone) => {
if (isDone) setKeyBackup(null); if (isDone) setKeyBackup(null);
requestClose(); requestClose();

View file

@ -116,7 +116,14 @@ export const accessSecretStorage = (title) => new Promise((resolve) => {
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>, <Text variant="s1" weight="medium">{title}</Text>,
() => <SecretStorageAccess onComplete={handleComplete} />, (requestClose) => (
<SecretStorageAccess
onComplete={(keyData) => {
handleComplete(keyData);
requestClose(requestClose);
}}
/>
),
() => { () => {
if (!isCompleted) resolve(null); if (!isCompleted) resolve(null);
}, },