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-group {
|
||||||
--emoji-padding: 6px;
|
--emoji-padding: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
import { hasCrossSigningAccountData } from '../../../util/matrixUtil';
|
import { hasCrossSigningAccountData } from '../../../util/matrixUtil';
|
||||||
import { copyToClipboard } from '../../../util/common';
|
import { copyToClipboard } from '../../../util/common';
|
||||||
|
import { storePrivateKey } from '../../../client/state/secretStorageKeys';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
|
@ -57,6 +58,12 @@ function CrossSigningSetup() {
|
||||||
setGenWithPhrase(typeof securityPhrase === 'string');
|
setGenWithPhrase(typeof securityPhrase === 'string');
|
||||||
const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
|
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 = {
|
const bootstrapSSOpts = {
|
||||||
createSecretStorageKey: async () => recoveryKey,
|
createSecretStorageKey: async () => recoveryKey,
|
||||||
setupNewKeyBackup: true,
|
setupNewKeyBackup: true,
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect } from 'react';
|
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 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 Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
|
@ -9,36 +16,239 @@ import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import InfoCard from '../../atoms/card/InfoCard';
|
import InfoCard from '../../atoms/card/InfoCard';
|
||||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||||
|
|
||||||
|
import SecretStorageAccess from './SecretStorageAccess';
|
||||||
|
|
||||||
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
||||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
|
import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
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() {
|
function KeyBackup() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const isCSEnabled = useCrossSigningStatus();
|
const isCSEnabled = useCrossSigningStatus();
|
||||||
const [keyBackup, setKeyBackup] = useState(undefined);
|
const [keyBackup, setKeyBackup] = useState(undefined);
|
||||||
|
const mountStore = useStore();
|
||||||
|
|
||||||
|
const fetchKeyBackupVersion = async () => {
|
||||||
|
const info = await mx.getKeyBackupVersion();
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
setKeyBackup(info);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
mountStore.setItem(true);
|
||||||
mx.getKeyBackupVersion().then((info) => {
|
fetchKeyBackupVersion();
|
||||||
if (!isMounted) return;
|
|
||||||
setKeyBackup(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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 = () => {
|
const renderOptions = () => {
|
||||||
if (keyBackup === undefined) return <Spinner size="small" />;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton src={DownloadIC} variant="positive" onClick={() => alert('restore')} tooltip="Restore backup" />
|
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
|
||||||
<IconButton src={BinIC} onClick={() => alert('delete')} tooltip="Delete 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 { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
|
@ -14,8 +15,8 @@ import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
function SecretStorageAccess({ onComplete }) {
|
function SecretStorageAccess({ onComplete }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const sSKeyId = mx.getAccountData('m.secret_storage.default_key').getContent().key;
|
const sSKeyId = getDefaultSSKey();
|
||||||
const sSKeyInfo = mx.getAccountData(`m.secret_storage.key.${sSKeyId}`).getContent();
|
const sSKeyInfo = getSSKeyInfo(sSKeyId);
|
||||||
const isPassphrase = !!sSKeyInfo.passphrase;
|
const isPassphrase = !!sSKeyInfo.passphrase;
|
||||||
const [withPhrase, setWithPhrase] = useState(isPassphrase);
|
const [withPhrase, setWithPhrase] = useState(isPassphrase);
|
||||||
const [process, setProcess] = useState(false);
|
const [process, setProcess] = useState(false);
|
||||||
|
@ -40,7 +41,12 @@ function SecretStorageAccess({ onComplete }) {
|
||||||
setProcess(false);
|
setProcess(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onComplete({ key, phrase, decodedKey });
|
onComplete({
|
||||||
|
keyId: sSKeyId,
|
||||||
|
key,
|
||||||
|
phrase,
|
||||||
|
decodedKey,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mountStore.getItem()) return;
|
if (!mountStore.getItem()) return;
|
||||||
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
|
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import RoomList from './state/RoomList';
|
||||||
import AccountData from './state/AccountData';
|
import AccountData from './state/AccountData';
|
||||||
import RoomsInput from './state/RoomsInput';
|
import RoomsInput from './state/RoomsInput';
|
||||||
import Notifications from './state/Notifications';
|
import Notifications from './state/Notifications';
|
||||||
|
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||||
|
|
||||||
global.Olm = require('@matrix-org/olm');
|
global.Olm = require('@matrix-org/olm');
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ class InitMatrix extends EventEmitter {
|
||||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||||
deviceId: secret.deviceId,
|
deviceId: secret.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
|
cryptoCallbacks,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.matrixClient.initCrypto();
|
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');
|
const masterKeyData = mx.getAccountData('m.cross_signing.master');
|
||||||
return !!masterKeyData;
|
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