Add key backup

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-04-15 19:20:23 +05:30
parent d0cec1108a
commit 88e0321d28
8 changed files with 321 additions and 15 deletions

View file

@ -91,6 +91,10 @@
}
}
.emoji-row {
display: flex;
}
.emoji-group {
--emoji-padding: 6px;
position: relative;

View file

@ -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,

View file

@ -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" />
</>
);
};

View 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);
}
}

View file

@ -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'}`);

View file

@ -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();

View 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,
};

View file

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