Add key backup
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
d0cec1108a
commit
88e0321d28
8 changed files with 321 additions and 15 deletions
|
@ -91,6 +91,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.emoji-group {
|
||||
--emoji-padding: 6px;
|
||||
position: relative;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
<div className="key-backup__create">
|
||||
{done === false && (
|
||||
<div>
|
||||
<Spinner size="small" />
|
||||
<Text>Creating backup...</Text>
|
||||
</div>
|
||||
)}
|
||||
{done === true && (
|
||||
<>
|
||||
<Text variant="h1">{twemojify('👍')}</Text>
|
||||
<Text>Successfully created backup</Text>
|
||||
</>
|
||||
)}
|
||||
{done === null && (
|
||||
<>
|
||||
<Text>Failed to create backup</Text>
|
||||
<Button onClick={doBackup}>Retry</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="key-backup__restore">
|
||||
{done === false && (
|
||||
<div>
|
||||
<Spinner size="small" />
|
||||
<Text>Restoring backup...</Text>
|
||||
</div>
|
||||
)}
|
||||
{done === true && (
|
||||
<>
|
||||
<Text variant="h1">{twemojify('👍')}</Text>
|
||||
<Text>Successfully restored backup</Text>
|
||||
</>
|
||||
)}
|
||||
{done === null && (
|
||||
<>
|
||||
<Text>Failed to restore backup</Text>
|
||||
<Button onClick={restoreBackup}>Retry</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="key-backup__delete">
|
||||
<Text variant="h1">{twemojify('🗑')}</Text>
|
||||
<Text weight="medium">Deleting key backup is permanent.</Text>
|
||||
<Text>All encrypted messages keys stored on server will be deleted.</Text>
|
||||
{
|
||||
isDeleting
|
||||
? <Spinner size="small" />
|
||||
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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(
|
||||
<Text variant="s1" weight="medium">{title}</Text>,
|
||||
() => <SecretStorageAccess onComplete={handleComplete} />,
|
||||
);
|
||||
};
|
||||
|
||||
const openCreateKeyBackup = () => {
|
||||
const createKeyBackup = (keyData) => {
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Create Key Backup</Text>,
|
||||
() => <CreateKeyBackupDialog keyData={keyData} />,
|
||||
() => fetchKeyBackupVersion(),
|
||||
);
|
||||
};
|
||||
accessSecretStorage('Create Key Backup', createKeyBackup);
|
||||
};
|
||||
|
||||
const openRestoreKeyBackup = () => {
|
||||
const restoreKeyBackup = (keyData) => {
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
|
||||
() => <RestoreKeyBackupDialog keyData={keyData} backupInfo={keyBackup} />,
|
||||
);
|
||||
};
|
||||
accessSecretStorage('Restore Key Backup', restoreKeyBackup);
|
||||
};
|
||||
|
||||
const openDeleteKeyBackup = () => openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
|
||||
(requestClose) => (
|
||||
<DeleteKeyBackupDialog
|
||||
version={keyBackup.version}
|
||||
requestClose={(isDone) => {
|
||||
if (isDone) setKeyBackup(null);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
const renderOptions = () => {
|
||||
if (keyBackup === undefined) return <Spinner size="small" />;
|
||||
if (keyBackup === null) return <Button variant="primary" onClick={() => alert('create')}>Create Backup</Button>;
|
||||
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
|
||||
return (
|
||||
<>
|
||||
<IconButton src={DownloadIC} variant="positive" onClick={() => alert('restore')} tooltip="Restore backup" />
|
||||
<IconButton src={BinIC} onClick={() => alert('delete')} tooltip="Delete backup" />
|
||||
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
|
||||
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
27
src/app/organisms/settings/KeyBackup.scss
Normal file
27
src/app/organisms/settings/KeyBackup.scss
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'}`);
|
||||
|
|
|
@ -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();
|
||||
|
|
32
src/client/state/secretStorageKeys.js
Normal file
32
src/client/state/secretStorageKeys.js
Normal file
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue