From 101134e313690c8620144f7e97b1bd82950d9540 Mon Sep 17 00:00:00 2001 From: Ajay Bura Date: Sun, 24 Apr 2022 13:17:36 +0530 Subject: [PATCH] Add SSSS and key backup --- src/app/organisms/settings/AuthRequest.jsx | 117 ++++++++++++++++++ src/app/organisms/settings/AuthRequest.scss | 12 ++ src/app/organisms/settings/CrossSigning.jsx | 53 ++++---- src/app/organisms/settings/CrossSigning.scss | 3 +- src/app/organisms/settings/DeviceManage.jsx | 39 ++---- src/app/organisms/settings/KeyBackup.jsx | 17 ++- .../settings/SecretStorageAccess.jsx | 9 +- 7 files changed, 188 insertions(+), 62 deletions(-) create mode 100644 src/app/organisms/settings/AuthRequest.jsx create mode 100644 src/app/organisms/settings/AuthRequest.scss diff --git a/src/app/organisms/settings/AuthRequest.jsx b/src/app/organisms/settings/AuthRequest.jsx new file mode 100644 index 00000000..ca07c2a2 --- /dev/null +++ b/src/app/organisms/settings/AuthRequest.jsx @@ -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 ( +
+
+ + {status.ongoing && } + {status.error && {status.error}} + {(status === false || status.error) && } + +
+ ); +} +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} 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( + {title}, + (requestClose) => ( + { + isCompleted = true; + resolve(done); + requestClose(); + }} + makeRequest={makeRequest} + /> + ), + () => { + if (!isCompleted) resolve(false); + }, + ); + }); + } +}; + +export default AuthRequest; diff --git a/src/app/organisms/settings/AuthRequest.scss b/src/app/organisms/settings/AuthRequest.scss new file mode 100644 index 00000000..35e95bf2 --- /dev/null +++ b/src/app/organisms/settings/AuthRequest.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/app/organisms/settings/CrossSigning.jsx b/src/app/organisms/settings/CrossSigning.jsx index 761c0096..9213e9da 100644 --- a/src/app/organisms/settings/CrossSigning.jsx +++ b/src/app/organisms/settings/CrossSigning.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-one-expression-per-line */ import React, { useState } from 'react'; import './CrossSigning.scss'; import FileSaver from 'file-saver'; @@ -6,7 +7,6 @@ import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import { openReusableDialog } from '../../../client/action/navigation'; -import { hasCrossSigningAccountData } from '../../../util/matrixUtil'; import { copyToClipboard } from '../../../util/common'; import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys'; @@ -16,6 +16,24 @@ 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) => ( +
+ {twemojify('❌')} + Failed to setup cross signing. Please try again. + +
+ ); + + openReusableDialog( + Setup cross signing, + renderFailure, + ); +}; + const securityKeyDialog = (key) => { const downloadKey = () => { const blob = new Blob([key.encodedPrivateKey], { @@ -66,28 +84,19 @@ function CrossSigningSetup() { }); const authUploadDeviceSigningKeys = async (makeRequest) => { - try { - const password = prompt('Password', ''); - await makeRequest({ - type: 'm.login.password', - password, - identifier: { - type: 'm.id.user', - user: mx.getUserId(), - }, - }); - } catch (e) { - console.log(e); - // TODO: handle error - } + 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, }); - - securityKeyDialog(recoveryKey); }; const validator = (values) => { @@ -112,7 +121,7 @@ function CrossSigningSetup() {
- We will generate a Security Key, + We will generate a Security Key, which you can use to manage messages backup and session verification. {genWithPhrase !== false && } @@ -133,7 +142,7 @@ function CrossSigningSetup() { disabled={genWithPhrase !== undefined} > - Alternatively you can also set a Security Phrase + Alternatively you can also set a Security Phrase so you don't have to remember long Security Key, and optionally save the Key as backup. @@ -181,7 +190,8 @@ function CrossSigningReset() { Anyone you have verified with will see security alerts and your message backup will lost. 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 Security Key or Phrase and + every session you can cross-sign from.
@@ -196,12 +206,13 @@ const resetDialog = () => { }; function CrossSignin() { + const isCSEnabled = useCrossSigningStatus(); return ( Setup to verify and keep track of all your sessions. Also required to backup encrypted message.} options={( - hasCrossSigningAccountData() + isCSEnabled ? : )} diff --git a/src/app/organisms/settings/CrossSigning.scss b/src/app/organisms/settings/CrossSigning.scss index 494fa977..b4b606d0 100644 --- a/src/app/organisms/settings/CrossSigning.scss +++ b/src/app/organisms/settings/CrossSigning.scss @@ -45,10 +45,11 @@ } } +.cross-signing__failure, .cross-signing__reset { padding: var(--sp-normal); padding-top: var(--sp-extra-loose); & > .text { - padding-bottom: var(--sp-loose); + padding-bottom: var(--sp-normal); } } \ No newline at end of file diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx index ea768fdf..5c60bf0a 100644 --- a/src/app/organisms/settings/DeviceManage.jsx +++ b/src/app/organisms/settings/DeviceManage.jsx @@ -17,6 +17,8 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; +import { authRequest } from './AuthRequest'; + import { useStore } from '../../hooks/useStore'; import { useDeviceList } from '../../hooks/useDeviceList'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; @@ -71,38 +73,15 @@ function DeviceManage() { } }; - const handleRemove = async (device, auth = undefined) => { - if (auth === undefined - ? window.confirm(`You are about to logout "${device.display_name}" session.`) - : true - ) { + const handleRemove = async (device) => { + if (window.confirm(`You are about to logout "${device.display_name}" session.`)) { addToProcessing(device); - try { + await authRequest(`Logout "${device.display_name}"`, async (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; - removeFromProcessing(device); - } + }); + + if (!mountStore.getItem()) return; + removeFromProcessing(device); } }; diff --git a/src/app/organisms/settings/KeyBackup.jsx b/src/app/organisms/settings/KeyBackup.jsx index 50080a6e..5d2f4ed7 100644 --- a/src/app/organisms/settings/KeyBackup.jsx +++ b/src/app/organisms/settings/KeyBackup.jsx @@ -82,7 +82,7 @@ CreateKeyBackupDialog.propTypes = { keyData: PropTypes.shape({}).isRequired, }; -function RestoreKeyBackupDialog({ keyData, backupInfo }) { +function RestoreKeyBackupDialog({ keyData }) { const [status, setStatus] = useState(false); const mx = initMatrix.matrixClient; const mountStore = useStore(); @@ -103,6 +103,7 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) { }; try { + const backupInfo = await mx.getKeyBackupVersion(); const info = await mx.restoreKeyBackupWithSecretStorage( backupInfo, undefined, @@ -115,7 +116,7 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) { if (!mountStore.getItem()) return; if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') { 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 { setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' }); } @@ -152,10 +153,9 @@ function RestoreKeyBackupDialog({ keyData, backupInfo }) { } RestoreKeyBackupDialog.propTypes = { keyData: PropTypes.shape({}).isRequired, - backupInfo: PropTypes.shape({}).isRequired, }; -function DeleteKeyBackupDialog({ version, requestClose }) { +function DeleteKeyBackupDialog({ requestClose }) { const [isDeleting, setIsDeleting] = useState(false); const mx = initMatrix.matrixClient; const mountStore = useStore(); @@ -164,7 +164,8 @@ function DeleteKeyBackupDialog({ version, requestClose }) { const deleteBackup = async () => { setIsDeleting(true); try { - await mx.deleteKeyBackupVersion(version); + const backupInfo = await mx.getKeyBackupVersion(); + if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version); if (!mountStore.getItem()) return; requestClose(true); } catch { @@ -187,7 +188,6 @@ function DeleteKeyBackupDialog({ version, requestClose }) { ); } DeleteKeyBackupDialog.propTypes = { - version: PropTypes.string.isRequired, requestClose: PropTypes.func.isRequired, }; @@ -217,7 +217,7 @@ function KeyBackup() { return () => { mx.removeListener('accountData', handleAccountData); }; - }, []); + }, [isCSEnabled]); const openCreateKeyBackup = async () => { const keyData = await accessSecretStorage('Create Key Backup'); @@ -236,7 +236,7 @@ function KeyBackup() { openReusableDialog( Restore Key Backup, - () => , + () => , ); }; @@ -244,7 +244,6 @@ function KeyBackup() { Delete Key Backup, (requestClose) => ( { if (isDone) setKeyBackup(null); requestClose(); diff --git a/src/app/organisms/settings/SecretStorageAccess.jsx b/src/app/organisms/settings/SecretStorageAccess.jsx index a381cc74..f0131b14 100644 --- a/src/app/organisms/settings/SecretStorageAccess.jsx +++ b/src/app/organisms/settings/SecretStorageAccess.jsx @@ -116,7 +116,14 @@ export const accessSecretStorage = (title) => new Promise((resolve) => { openReusableDialog( {title}, - () => , + (requestClose) => ( + { + handleComplete(keyData); + requestClose(requestClose); + }} + /> + ), () => { if (!isCompleted) resolve(null); },