Add export E2E key (#178)

This commit is contained in:
Ajay Bura 2021-12-06 10:22:45 +05:30
parent 5b109c2b79
commit 50db137dea
10 changed files with 411 additions and 111 deletions

View file

@ -1,108 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import './ImportE2ERoomKeys.scss';
import EventEmitter from 'events';
import initMatrix from '../../../client/initMatrix';
import decryptMegolmKeyFile from '../../../util/decryptE2ERoomKeys';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
const viewEvent = new EventEmitter();
async function tryDecrypt(file, password) {
try {
const arrayBuffer = await file.arrayBuffer();
viewEvent.emit('importing', true);
viewEvent.emit('status', 'Decrypting file...');
const keys = await decryptMegolmKeyFile(arrayBuffer, password);
viewEvent.emit('status', 'Decrypting messages...');
await initMatrix.matrixClient.importRoomKeys(JSON.parse(keys));
viewEvent.emit('status', null);
viewEvent.emit('importing', false);
} catch (e) {
viewEvent.emit('status', e.friendlyText || 'Something went wrong!');
viewEvent.emit('importing', false);
}
}
function ImportE2ERoomKeys() {
const [keyFile, setKeyFile] = useState(null);
const [status, setStatus] = useState(null);
const [isImporting, setIsImporting] = useState(false);
const inputRef = useRef(null);
const passwordRef = useRef(null);
useEffect(() => {
const handleIsImporting = (isImp) => setIsImporting(isImp);
const handleStatus = (msg) => setStatus(msg);
viewEvent.on('importing', handleIsImporting);
viewEvent.on('status', handleStatus);
return () => {
viewEvent.removeListener('importing', handleIsImporting);
viewEvent.removeListener('status', handleStatus);
};
}, []);
function importE2ERoomKeys() {
const password = passwordRef.current.value;
if (password === '' || keyFile === null) return;
if (isImporting) return;
tryDecrypt(keyFile, password);
}
function handleFileChange(e) {
const file = e.target.files.item(0);
passwordRef.current.value = '';
setKeyFile(file);
setStatus(null);
}
function removeImportKeysFile() {
inputRef.current.value = null;
passwordRef.current.value = null;
setKeyFile(null);
setStatus(null);
}
useEffect(() => {
if (!isImporting && status === null) {
removeImportKeysFile();
}
}, [isImporting, status]);
return (
<div className="import-e2e-room-keys">
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" />
<form className="import-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); importE2ERoomKeys(); }}>
{ keyFile !== null && (
<div className="import-e2e-room-keys__file">
<IconButton onClick={removeImportKeysFile} src={CirclePlusIC} tooltip="Remove file" />
<Text>{keyFile.name}</Text>
</div>
)}
{keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>}
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Button disabled={isImporting} variant="primary" type="submit">Decrypt</Button>
</form>
{ isImporting && status !== null && (
<div className="import-e2e-room-keys__process">
<Spinner size="small" />
<Text variant="b2">{status}</Text>
</div>
)}
{!isImporting && status !== null && <Text className="import-e2e-room-keys__error" variant="b2">{status}</Text>}
</div>
);
}
export default ImportE2ERoomKeys;

View file

@ -0,0 +1,100 @@
import React, { useState, useEffect, useRef } from 'react';
import './ExportE2ERoomKeys.scss';
import FileSaver from 'file-saver';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
function ExportE2ERoomKeys() {
const isMountStore = useStore();
const [status, setStatus] = useState({
isOngoing: false,
msg: null,
type: cons.status.PRE_FLIGHT,
});
const passwordRef = useRef(null);
const confirmPasswordRef = useRef(null);
const exportE2ERoomKeys = async () => {
const password = passwordRef.current.value;
if (password !== confirmPasswordRef.current.value) {
setStatus({
isOngoing: false,
msg: 'Password does not match.',
type: cons.status.ERROR,
});
return;
}
setStatus({
isOngoing: true,
msg: 'Getting keys...',
type: cons.status.IN_FLIGHT,
});
try {
const keys = await initMatrix.matrixClient.exportRoomKeys();
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Encrypting keys...',
type: cons.status.IN_FLIGHT,
});
}
const encKeys = await encryptMegolmKeyFile(JSON.stringify(keys), password);
const blob = new Blob([encKeys], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'cinny-keys.txt');
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: 'Successfully exported all keys.',
type: cons.status.SUCCESS,
});
}
} catch (e) {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: e.friendlyText || 'Failed to export keys. Please try again.',
type: cons.status.ERROR,
});
}
}
};
useEffect(() => {
isMountStore.setItem(true);
return () => {
isMountStore.setItem(false);
};
}, []);
return (
<div className="export-e2e-room-keys">
<form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button>
</form>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">
<Spinner size="small" />
<Text variant="b2">{status.msg}</Text>
</div>
)}
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
</div>
);
}
export default ExportE2ERoomKeys;

View file

@ -0,0 +1,28 @@
.export-e2e-room-keys {
margin-top: var(--sp-extra-tight);
&__form {
display: flex;
& > .input-container {
flex: 1;
min-width: 0;
}
& > *:nth-child(2) {
margin: 0 var(--sp-tight);
}
}
&__process {
margin-top: var(--sp-tight);
display: flex;
justify-content: center;
align-items: center;
& .text {
margin: 0 var(--sp-tight);
}
}
&__error {
margin-top: var(--sp-tight);
color: var(--tc-danger-high);
}
}

View file

@ -0,0 +1,133 @@
import React, { useState, useEffect, useRef } from 'react';
import './ImportE2ERoomKeys.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
import { useStore } from '../../hooks/useStore';
function ImportE2ERoomKeys() {
const isMountStore = useStore();
const [keyFile, setKeyFile] = useState(null);
const [status, setStatus] = useState({
isOngoing: false,
msg: null,
type: cons.status.PRE_FLIGHT,
});
const inputRef = useRef(null);
const passwordRef = useRef(null);
async function tryDecrypt(file, password) {
try {
const arrayBuffer = await file.arrayBuffer();
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Decrypting file...',
type: cons.status.IN_FLIGHT,
});
}
const keys = await decryptMegolmKeyFile(arrayBuffer, password);
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Decrypting messages...',
type: cons.status.IN_FLIGHT,
});
}
await initMatrix.matrixClient.importRoomKeys(JSON.parse(keys));
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: 'Successfully imported all keys.',
type: cons.status.SUCCESS,
});
inputRef.current.value = null;
passwordRef.current.value = null;
}
} catch (e) {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: e.friendlyText || 'Failed to decrypt keys. Please try again.',
type: cons.status.ERROR,
});
}
}
}
const importE2ERoomKeys = () => {
const password = passwordRef.current.value;
if (password === '' || keyFile === null) return;
if (status.isOngoing) return;
tryDecrypt(keyFile, password);
};
const handleFileChange = (e) => {
const file = e.target.files.item(0);
passwordRef.current.value = '';
setKeyFile(file);
setStatus({
isOngoing: false,
msg: null,
type: cons.status.PRE_FLIGHT,
});
};
const removeImportKeysFile = () => {
if (status.isOngoing) return;
inputRef.current.value = null;
passwordRef.current.value = null;
setKeyFile(null);
setStatus({
isOngoing: false,
msg: null,
type: cons.status.PRE_FLIGHT,
});
};
useEffect(() => {
isMountStore.setItem(true);
return () => {
isMountStore.setItem(false);
};
}, []);
return (
<div className="import-e2e-room-keys">
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" />
<form className="import-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); importE2ERoomKeys(); }}>
{ keyFile !== null && (
<div className="import-e2e-room-keys__file">
<IconButton onClick={removeImportKeysFile} src={CirclePlusIC} tooltip="Remove file" />
<Text>{keyFile.name}</Text>
</div>
)}
{keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>}
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Decrypt</Button>
</form>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">
<Spinner size="small" />
<Text variant="b2">{status.msg}</Text>
</div>
)}
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
</div>
);
}
export default ImportE2ERoomKeys;

View file

@ -58,6 +58,10 @@
}
&__error {
margin-top: var(--sp-tight);
color: var(--bg-danger);
color: var(--tc-danger-high);
}
&__success {
margin-top: var(--sp-tight);
color: var(--tc-positive-high);
}
}