Unverified sessions
+ {!isCSEnabled && (
+
+
+
+ )}
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
diff --git a/src/app/organisms/settings/KeyBackup.jsx b/src/app/organisms/settings/KeyBackup.jsx
new file mode 100644
index 00000000..5d2f4ed7
--- /dev/null
+++ b/src/app/organisms/settings/KeyBackup.jsx
@@ -0,0 +1,288 @@
+/* eslint-disable react/prop-types */
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import './KeyBackup.scss';
+import { twemojify } from '../../../util/twemojify';
+
+import initMatrix from '../../../client/initMatrix';
+import { openReusableDialog } from '../../../client/action/navigation';
+import { deletePrivateKey } from '../../../client/state/secretStorageKeys';
+
+import Text from '../../atoms/text/Text';
+import Button from '../../atoms/button/Button';
+import IconButton from '../../atoms/button/IconButton';
+import Spinner from '../../atoms/spinner/Spinner';
+import InfoCard from '../../atoms/card/InfoCard';
+import SettingTile from '../../molecules/setting-tile/SettingTile';
+
+import { accessSecretStorage } from './SecretStorageAccess';
+
+import InfoIC from '../../../../public/res/ic/outlined/info.svg';
+import BinIC from '../../../../public/res/ic/outlined/bin.svg';
+import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
+
+import { useStore } from '../../hooks/useStore';
+import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
+
+function CreateKeyBackupDialog({ keyData }) {
+ const [done, setDone] = useState(false);
+ const mx = initMatrix.matrixClient;
+ const mountStore = useStore();
+
+ const doBackup = async () => {
+ setDone(false);
+ let info;
+
+ try {
+ info = await mx.prepareKeyBackupVersion(
+ null,
+ { secureSecretStorage: true },
+ );
+ info = await mx.createKeyBackupVersion(info);
+ await mx.scheduleAllGroupSessionsForBackup();
+ if (!mountStore.getItem()) return;
+ setDone(true);
+ } catch (e) {
+ deletePrivateKey(keyData.keyId);
+ await mx.deleteKeyBackupVersion(info.version);
+ if (!mountStore.getItem()) return;
+ setDone(null);
+ }
+ };
+
+ useEffect(() => {
+ mountStore.setItem(true);
+ doBackup();
+ }, []);
+
+ return (
+
+ {done === false && (
+
+
+ Creating backup...
+
+ )}
+ {done === true && (
+ <>
+
{twemojify('✅')}
+
Successfully created backup
+ >
+ )}
+ {done === null && (
+ <>
+
Failed to create backup
+
Retry
+ >
+ )}
+
+ );
+}
+CreateKeyBackupDialog.propTypes = {
+ keyData: PropTypes.shape({}).isRequired,
+};
+
+function RestoreKeyBackupDialog({ keyData }) {
+ const [status, setStatus] = useState(false);
+ const mx = initMatrix.matrixClient;
+ const mountStore = useStore();
+
+ const restoreBackup = async () => {
+ setStatus(false);
+
+ let meBreath = true;
+ const progressCallback = (progress) => {
+ if (!progress.successes) return;
+ if (meBreath === false) return;
+ meBreath = false;
+ setTimeout(() => {
+ meBreath = true;
+ }, 200);
+
+ setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` });
+ };
+
+ try {
+ const backupInfo = await mx.getKeyBackupVersion();
+ const info = await mx.restoreKeyBackupWithSecretStorage(
+ backupInfo,
+ undefined,
+ undefined,
+ { progressCallback },
+ );
+ if (!mountStore.getItem()) return;
+ setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
+ } catch (e) {
+ 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' });
+ } else {
+ setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
+ }
+ }
+ };
+
+ useEffect(() => {
+ mountStore.setItem(true);
+ restoreBackup();
+ }, []);
+
+ return (
+
+ {(status === false || status.message) && (
+
+
+ {status.message ?? 'Restoring backup keys...'}
+
+ )}
+ {status.done && (
+ <>
+
{twemojify('✅')}
+
{status.done}
+ >
+ )}
+ {status.error && (
+ <>
+
{status.error}
+
Retry
+ >
+ )}
+
+ );
+}
+RestoreKeyBackupDialog.propTypes = {
+ keyData: PropTypes.shape({}).isRequired,
+};
+
+function DeleteKeyBackupDialog({ requestClose }) {
+ const [isDeleting, setIsDeleting] = useState(false);
+ const mx = initMatrix.matrixClient;
+ const mountStore = useStore();
+ mountStore.setItem(true);
+
+ const deleteBackup = async () => {
+ setIsDeleting(true);
+ try {
+ const backupInfo = await mx.getKeyBackupVersion();
+ if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version);
+ if (!mountStore.getItem()) return;
+ requestClose(true);
+ } catch {
+ if (!mountStore.getItem()) return;
+ setIsDeleting(false);
+ }
+ };
+
+ return (
+
+ {twemojify('🗑')}
+ Deleting key backup is permanent.
+ All encrypted messages keys stored on server will be deleted.
+ {
+ isDeleting
+ ?
+ : Delete
+ }
+
+ );
+}
+DeleteKeyBackupDialog.propTypes = {
+ requestClose: PropTypes.func.isRequired,
+};
+
+function KeyBackup() {
+ const mx = initMatrix.matrixClient;
+ const isCSEnabled = useCrossSigningStatus();
+ const [keyBackup, setKeyBackup] = useState(undefined);
+ const mountStore = useStore();
+
+ const fetchKeyBackupVersion = async () => {
+ const info = await mx.getKeyBackupVersion();
+ if (!mountStore.getItem()) return;
+ setKeyBackup(info);
+ };
+
+ useEffect(() => {
+ mountStore.setItem(true);
+ fetchKeyBackupVersion();
+
+ const handleAccountData = (event) => {
+ if (event.getType() === 'm.megolm_backup.v1') {
+ fetchKeyBackupVersion();
+ }
+ };
+
+ mx.on('accountData', handleAccountData);
+ return () => {
+ mx.removeListener('accountData', handleAccountData);
+ };
+ }, [isCSEnabled]);
+
+ const openCreateKeyBackup = async () => {
+ const keyData = await accessSecretStorage('Create Key Backup');
+ if (keyData === null) return;
+
+ openReusableDialog(
+
Create Key Backup ,
+ () =>
,
+ () => fetchKeyBackupVersion(),
+ );
+ };
+
+ const openRestoreKeyBackup = async () => {
+ const keyData = await accessSecretStorage('Restore Key Backup');
+ if (keyData === null) return;
+
+ openReusableDialog(
+
Restore Key Backup ,
+ () =>
,
+ );
+ };
+
+ const openDeleteKeyBackup = () => openReusableDialog(
+
Delete Key Backup ,
+ (requestClose) => (
+
{
+ if (isDone) setKeyBackup(null);
+ requestClose();
+ }}
+ />
+ ),
+ );
+
+ const renderOptions = () => {
+ if (keyBackup === undefined) return ;
+ if (keyBackup === null) return Create Backup ;
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ return (
+
+ Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
+ {!isCSEnabled && (
+
+ )}
+ >
+ )}
+ options={isCSEnabled ? renderOptions() : null}
+ />
+ );
+}
+
+export default KeyBackup;
diff --git a/src/app/organisms/settings/KeyBackup.scss b/src/app/organisms/settings/KeyBackup.scss
new file mode 100644
index 00000000..1f2b9b66
--- /dev/null
+++ b/src/app/organisms/settings/KeyBackup.scss
@@ -0,0 +1,27 @@
+.key-backup__create,
+.key-backup__restore {
+ padding: var(--sp-normal);
+
+ & > div {
+ padding: var(--sp-normal) 0;
+ display: flex;
+ align-items: center;
+
+ & > .text {
+ margin: 0 var(--sp-normal);
+ }
+ }
+
+ & > .text {
+ margin-bottom: var(--sp-normal);
+ }
+}
+
+.key-backup__delete {
+ padding: var(--sp-normal);
+ padding-top: var(--sp-extra-loose);
+
+ & > .text {
+ padding-bottom: var(--sp-normal);
+ }
+}
\ No newline at end of file
diff --git a/src/app/organisms/settings/SecretStorageAccess.jsx b/src/app/organisms/settings/SecretStorageAccess.jsx
new file mode 100644
index 00000000..f0131b14
--- /dev/null
+++ b/src/app/organisms/settings/SecretStorageAccess.jsx
@@ -0,0 +1,133 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import './SecretStorageAccess.scss';
+import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
+
+import initMatrix from '../../../client/initMatrix';
+import { openReusableDialog } from '../../../client/action/navigation';
+import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
+import { storePrivateKey, hasPrivateKey, getPrivateKey } 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 { useStore } from '../../hooks/useStore';
+
+function SecretStorageAccess({ onComplete }) {
+ const mx = initMatrix.matrixClient;
+ const sSKeyId = getDefaultSSKey();
+ const sSKeyInfo = getSSKeyInfo(sSKeyId);
+ const isPassphrase = !!sSKeyInfo.passphrase;
+ const [withPhrase, setWithPhrase] = useState(isPassphrase);
+ const [process, setProcess] = useState(false);
+ const [error, setError] = useState(null);
+ const mountStore = useStore();
+ mountStore.setItem(true);
+
+ const toggleWithPhrase = () => setWithPhrase(!withPhrase);
+
+ const processInput = async ({ key, phrase }) => {
+ setProcess(true);
+ try {
+ const { salt, iterations } = sSKeyInfo.passphrase;
+ const privateKey = key
+ ? mx.keyBackupKeyFromRecoveryKey(key)
+ : await deriveKey(phrase, salt, iterations);
+ const isCorrect = await mx.checkSecretStorageKey(privateKey, sSKeyInfo);
+
+ if (!mountStore.getItem()) return;
+ if (!isCorrect) {
+ setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
+ setProcess(false);
+ return;
+ }
+ onComplete({
+ keyId: sSKeyId,
+ key,
+ phrase,
+ privateKey,
+ });
+ } catch (e) {
+ if (!mountStore.getItem()) return;
+ setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
+ setProcess(false);
+ }
+ };
+
+ const handleForm = async (e) => {
+ e.preventDefault();
+ const password = e.target.password.value;
+ if (password.trim() === '') return;
+ const data = {};
+ if (withPhrase) data.phrase = password;
+ else data.key = password;
+ processInput(data);
+ };
+
+ const handleChange = () => {
+ setError(null);
+ setProcess(false);
+ };
+
+ return (
+
+ );
+}
+SecretStorageAccess.propTypes = {
+ onComplete: PropTypes.func.isRequired,
+};
+
+/**
+ * @param {string} title Title of secret storage access dialog
+ * @returns {Promise} resolve to keyData or null
+ */
+export const accessSecretStorage = (title) => new Promise((resolve) => {
+ let isCompleted = false;
+ const defaultSSKey = getDefaultSSKey();
+ if (hasPrivateKey(defaultSSKey)) {
+ resolve({ keyId: defaultSSKey, privateKey: getPrivateKey(defaultSSKey) });
+ return;
+ }
+ const handleComplete = (keyData) => {
+ isCompleted = true;
+ storePrivateKey(keyData.keyId, keyData.privateKey);
+ resolve(keyData);
+ };
+
+ openReusableDialog(
+ {title} ,
+ (requestClose) => (
+ {
+ handleComplete(keyData);
+ requestClose(requestClose);
+ }}
+ />
+ ),
+ () => {
+ if (!isCompleted) resolve(null);
+ },
+ );
+});
+
+export default SecretStorageAccess;
diff --git a/src/app/organisms/settings/SecretStorageAccess.scss b/src/app/organisms/settings/SecretStorageAccess.scss
new file mode 100644
index 00000000..a7c0a9f6
--- /dev/null
+++ b/src/app/organisms/settings/SecretStorageAccess.scss
@@ -0,0 +1,20 @@
+.secret-storage-access {
+ 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;
+ }
+
+ &__btn {
+ display: flex;
+ justify-content: space-between;
+ }
+ & .donut-spinner {
+ margin-top: var(--sp-normal);
+ }
+}
\ No newline at end of file
diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx
index acfef5c9..6dbbffb2 100644
--- a/src/app/organisms/settings/Settings.jsx
+++ b/src/app/organisms/settings/Settings.jsx
@@ -26,6 +26,8 @@ import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/Impor
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor';
+import CrossSigning from './CrossSigning';
+import KeyBackup from './KeyBackup';
import DeviceManage from './DeviceManage';
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
@@ -168,18 +170,13 @@ function SecuritySection() {
return (
- Session Info
-
- Use this session ID-key combo to verify or manage this session.}
- />
+ Cross signing and backup
+
+
- Encryption
+ Export/Import encryption keys