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; + } +}