diff --git a/src/app/organisms/emoji-board/EmojiBoard.scss b/src/app/organisms/emoji-board/EmojiBoard.scss
index 1bc4553f..0db59d96 100644
--- a/src/app/organisms/emoji-board/EmojiBoard.scss
+++ b/src/app/organisms/emoji-board/EmojiBoard.scss
@@ -91,6 +91,10 @@
}
}
+.emoji-row {
+ display: flex;
+}
+
.emoji-group {
--emoji-padding: 6px;
position: relative;
diff --git a/src/app/organisms/settings/CrossSigning.jsx b/src/app/organisms/settings/CrossSigning.jsx
index 13d5bbbe..f5683f9c 100644
--- a/src/app/organisms/settings/CrossSigning.jsx
+++ b/src/app/organisms/settings/CrossSigning.jsx
@@ -8,6 +8,7 @@ import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { hasCrossSigningAccountData } from '../../../util/matrixUtil';
import { copyToClipboard } from '../../../util/common';
+import { storePrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -57,6 +58,12 @@ function CrossSigningSetup() {
setGenWithPhrase(typeof securityPhrase === 'string');
const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
+ const cSInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
+ const { keys, firstUse, crossSigningVerifiedBefore } = cSInfo;
+ mx.crypto.crossSigningInfo.keys = keys;
+ mx.crypto.crossSigningInfo.firstUse = firstUse;
+ mx.crypto.crossSigningVerifiedBefore = crossSigningVerifiedBefore;
+
const bootstrapSSOpts = {
createSecretStorageKey: async () => recoveryKey,
setupNewKeyBackup: true,
diff --git a/src/app/organisms/settings/KeyBackup.jsx b/src/app/organisms/settings/KeyBackup.jsx
index e57ad224..514feecb 100644
--- a/src/app/organisms/settings/KeyBackup.jsx
+++ b/src/app/organisms/settings/KeyBackup.jsx
@@ -1,6 +1,13 @@
+/* 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 { getDefaultSSKey } from '../../../util/matrixUtil';
+import { storePrivateKey, hasPrivateKey, getPrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -9,36 +16,239 @@ import Spinner from '../../atoms/spinner/Spinner';
import InfoCard from '../../atoms/card/InfoCard';
import SettingTile from '../../molecules/setting-tile/SettingTile';
+import SecretStorageAccess 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(
+ keyData.decodedKey,
+ { secureSecretStorage: true },
+ );
+ info = await mx.createKeyBackupVersion(info);
+ await mx.scheduleAllGroupSessionsForBackup();
+ if (!mountStore.getItem()) return;
+ setDone(true);
+ } catch (e) {
+ 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
+
+ >
+ )}
+
+ );
+}
+CreateKeyBackupDialog.propTypes = {
+ keyData: PropTypes.shape({}).isRequired,
+};
+
+function RestoreKeyBackupDialog({ keyData, backupInfo }) {
+ const [done, setDone] = useState(false);
+ const mx = initMatrix.matrixClient;
+ const mountStore = useStore();
+
+ const restoreBackup = async () => {
+ setDone(false);
+
+ try {
+ await mx.restoreKeyBackup(
+ keyData.decodedKey,
+ undefined,
+ undefined,
+ backupInfo,
+ );
+ if (!mountStore.getItem()) return;
+ setDone(true);
+ } catch (e) {
+ if (!mountStore.getItem()) return;
+ setDone(null);
+ }
+ };
+
+ useEffect(() => {
+ mountStore.setItem(true);
+ restoreBackup();
+ }, []);
+
+ return (
+
+ {done === false && (
+
+
+ Restoring backup...
+
+ )}
+ {done === true && (
+ <>
+
{twemojify('👍')}
+
Successfully restored backup
+ >
+ )}
+ {done === null && (
+ <>
+
Failed to restore backup
+
+ >
+ )}
+
+ );
+}
+RestoreKeyBackupDialog.propTypes = {
+ keyData: PropTypes.shape({}).isRequired,
+ backupInfo: PropTypes.shape({}).isRequired,
+};
+
+function DeleteKeyBackupDialog({ version, requestClose }) {
+ const [isDeleting, setIsDeleting] = useState(false);
+ const mx = initMatrix.matrixClient;
+ const mountStore = useStore();
+ mountStore.setItem(true);
+
+ const deleteBackup = async () => {
+ setIsDeleting(true);
+ try {
+ await mx.deleteKeyBackupVersion(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
+ ?
+ :
+ }
+
+ );
+}
+DeleteKeyBackupDialog.propTypes = {
+ version: PropTypes.string.isRequired,
+ 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(() => {
- let isMounted = true;
- mx.getKeyBackupVersion().then((info) => {
- if (!isMounted) return;
- setKeyBackup(info);
- });
-
- return () => {
- isMounted = false;
- };
+ mountStore.setItem(true);
+ fetchKeyBackupVersion();
}, []);
+ const accessSecretStorage = (title, onComplete) => {
+ const defaultSSKey = getDefaultSSKey();
+ if (hasPrivateKey(defaultSSKey)) {
+ onComplete({ decodedKey: getPrivateKey(defaultSSKey) });
+ return;
+ }
+ const handleComplete = (keyData) => {
+ storePrivateKey(keyData.keyId, keyData.decodedKey);
+ onComplete(keyData);
+ };
+
+ openReusableDialog(
+ {title},
+ () => ,
+ );
+ };
+
+ const openCreateKeyBackup = () => {
+ const createKeyBackup = (keyData) => {
+ openReusableDialog(
+ Create Key Backup,
+ () => ,
+ () => fetchKeyBackupVersion(),
+ );
+ };
+ accessSecretStorage('Create Key Backup', createKeyBackup);
+ };
+
+ const openRestoreKeyBackup = () => {
+ const restoreKeyBackup = (keyData) => {
+ openReusableDialog(
+ Restore Key Backup,
+ () => ,
+ );
+ };
+ accessSecretStorage('Restore Key Backup', restoreKeyBackup);
+ };
+
+ const openDeleteKeyBackup = () => openReusableDialog(
+ Delete Key Backup,
+ (requestClose) => (
+ {
+ if (isDone) setKeyBackup(null);
+ requestClose();
+ }}
+ />
+ ),
+ );
+
const renderOptions = () => {
if (keyBackup === undefined) return ;
- if (keyBackup === null) return ;
+ if (keyBackup === null) return ;
return (
<>
- alert('restore')} tooltip="Restore backup" />
- alert('delete')} tooltip="Delete backup" />
+
+
>
);
};
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
index 6acc0f5f..6756c4a8 100644
--- a/src/app/organisms/settings/SecretStorageAccess.jsx
+++ b/src/app/organisms/settings/SecretStorageAccess.jsx
@@ -4,6 +4,7 @@ import './SecretStorageAccess.scss';
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
import initMatrix from '../../../client/initMatrix';
+import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -14,8 +15,8 @@ import { useStore } from '../../hooks/useStore';
function SecretStorageAccess({ onComplete }) {
const mx = initMatrix.matrixClient;
- const sSKeyId = mx.getAccountData('m.secret_storage.default_key').getContent().key;
- const sSKeyInfo = mx.getAccountData(`m.secret_storage.key.${sSKeyId}`).getContent();
+ const sSKeyId = getDefaultSSKey();
+ const sSKeyInfo = getSSKeyInfo(sSKeyId);
const isPassphrase = !!sSKeyInfo.passphrase;
const [withPhrase, setWithPhrase] = useState(isPassphrase);
const [process, setProcess] = useState(false);
@@ -40,7 +41,12 @@ function SecretStorageAccess({ onComplete }) {
setProcess(false);
return;
}
- onComplete({ key, phrase, decodedKey });
+ onComplete({
+ keyId: sSKeyId,
+ key,
+ phrase,
+ decodedKey,
+ });
} catch (e) {
if (!mountStore.getItem()) return;
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js
index f6fc9eb2..0e5cd0d0 100644
--- a/src/client/initMatrix.js
+++ b/src/client/initMatrix.js
@@ -7,6 +7,7 @@ import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
import Notifications from './state/Notifications';
+import { cryptoCallbacks } from './state/secretStorageKeys';
global.Olm = require('@matrix-org/olm');
@@ -36,6 +37,7 @@ class InitMatrix extends EventEmitter {
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
deviceId: secret.deviceId,
timelineSupport: true,
+ cryptoCallbacks,
});
await this.matrixClient.initCrypto();
diff --git a/src/client/state/secretStorageKeys.js b/src/client/state/secretStorageKeys.js
new file mode 100644
index 00000000..d59761c1
--- /dev/null
+++ b/src/client/state/secretStorageKeys.js
@@ -0,0 +1,32 @@
+const secretStorageKeys = {};
+
+export function storePrivateKey(keyId, privateKey) {
+ if (privateKey instanceof Uint8Array === false) {
+ throw new Error('Unable to store, privateKey is invalid.');
+ }
+ secretStorageKeys[keyId] = privateKey;
+}
+
+export function hasPrivateKey(keyId) {
+ return secretStorageKeys[keyId] instanceof Uint8Array;
+}
+
+export function getPrivateKey(keyId) {
+ return secretStorageKeys[keyId];
+}
+
+export function deletePrivateKey(keyId) {
+ delete secretStorageKeys[keyId];
+}
+
+async function getSecretStorageKey({ keys }) {
+ const keyIds = Object.keys(keys);
+ const keyId = keyIds.find(hasPrivateKey);
+ if (!keyId) return undefined;
+ const privateKey = getPrivateKey(keyId);
+ return [keyId, privateKey];
+}
+
+export const cryptoCallbacks = {
+ getSecretStorageKey,
+};
diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js
index ecc2db49..1b67f7e6 100644
--- a/src/util/matrixUtil.js
+++ b/src/util/matrixUtil.js
@@ -180,3 +180,21 @@ export function hasCrossSigningAccountData() {
const masterKeyData = mx.getAccountData('m.cross_signing.master');
return !!masterKeyData;
}
+
+export function getDefaultSSKey() {
+ const mx = initMatrix.matrixClient;
+ try {
+ return mx.getAccountData('m.secret_storage.default_key').getContent().key;
+ } catch {
+ return undefined;
+ }
+}
+
+export function getSSKeyInfo(key) {
+ const mx = initMatrix.matrixClient;
+ try {
+ return mx.getAccountData(`m.secret_storage.key.${key}`).getContent();
+ } catch {
+ return undefined;
+ }
+}