Skip to content

Commit

Permalink
Merge pull request #647 from matrix-org/rav/megolm_backup_ui
Browse files Browse the repository at this point in the history
Import and export for Megolm session data
  • Loading branch information
richvdh committed Jan 25, 2017
2 parents 9cadc2f + ebe6cdd commit dc7a169
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 70 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -47,10 +47,12 @@
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
"commonmark": "^0.27.0",
"draft-js": "^0.8.1",
"draft-js-export-html": "^0.5.0",
"draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3",
"file-saver": "^1.3.3",
"filesize": "^3.1.2",
"flux": "^2.0.3",
"fuse.js": "^2.2.0",
Expand All @@ -59,7 +61,6 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"commonmark": "^0.27.0",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
Expand Down
175 changes: 131 additions & 44 deletions src/async-components/views/dialogs/ExportE2eKeysDialog.js
Expand Up @@ -14,71 +14,158 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import FileSaver from 'file-saver';
import React from 'react';

import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index';

import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
const PHASE_EDIT = 1;
const PHASE_EXPORTING = 2;

export default React.createClass({
displayName: 'ExportE2eKeysDialog',

propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired,
},

getInitialState: function() {
return {
collectedPassword: false,
phase: PHASE_EDIT,
errStr: null,
};
},

componentWillMount: function() {
this._unmounted = false;
},

componentWillUnmount: function() {
this._unmounted = true;
},

_onPassphraseFormSubmit: function(ev) {
ev.preventDefault();
console.log(this.refs.passphrase1.value);

const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) {
this.setState({errStr: 'Passphrases must match'});
return false;
}
if (!passphrase) {
this.setState({errStr: 'Passphrase must not be empty'});
return false;
}

this._startExport(passphrase);
return false;
},

render: function() {
let content;
if (!this.state.collectedPassword) {
content = (
<div className="mx_Dialog_content">
<p>
This process will allow you to export the keys for messages
you have received in encrypted rooms to a local file. You
will then be able to import the file into another Matrix
client in the future, so that client will also be able to
decrypt these messages.
</p>
<p>
The exported file will allow anyone who can read it to decrypt
any encrypted messages that you can see, so you should be
careful to keep it secure. To help with this, you should enter
a passphrase below, which will be used to encrypt the exported
data. It will only be possible to import the data by using the
same passphrase.
</p>
<form onSubmit={this._onPassphraseFormSubmit}>
<div className="mx_TextInputDialog_label">
<label htmlFor="passphrase1">Enter passphrase</label>
</div>
<div>
<input ref="passphrase1" id="passphrase1"
className="mx_TextInputDialog_input"
autoFocus={true} size="64" type="password"/>
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary" type="submit" value="Export" />
</div>
</form>
</div>
_startExport: function(passphrase) {
// extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones.
Promise.resolve().then(() => {
return this.props.matrixClient.exportRoomKeys();
}).then((k) => {
return MegolmExportEncryption.encryptMegolmKeyFile(
JSON.stringify(k), passphrase
);
}
}).then((f) => {
const blob = new Blob([f], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'riot-keys.txt');
this.props.onFinished(true);
}).catch((e) => {
if (this._unmounted) {
return;
}
this.setState({
errStr: e.message,
phase: PHASE_EDIT,
});
});

this.setState({
errStr: null,
phase: PHASE_EXPORTING,
});
},

render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');

const disableForm = (this.state.phase === PHASE_EXPORTING);

return (
<div className="mx_exportE2eKeysDialog">
<div className="mx_Dialog_title">
Export room keys
</div>
{content}
</div>
<BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished}
title="Export room keys"
>
<form onSubmit={this._onPassphraseFormSubmit}>
<div className="mx_Dialog_content">
<p>
This process allows you to export the keys for messages
you have received in encrypted rooms to a local file. You
will then be able to import the file into another Matrix
client in the future, so that client will also be able to
decrypt these messages.
</p>
<p>
The exported file will allow anyone who can read it to decrypt
any encrypted messages that you can see, so you should be
careful to keep it secure. To help with this, you should enter
a passphrase below, which will be used to encrypt the exported
data. It will only be possible to import the data by using the
same passphrase.
</p>
<div className='error'>
{this.state.errStr}
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase1'>
Enter passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase1' id='passphrase1'
autoFocus={true} size='64' type='password'
disabled={disableForm}
/>
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase2'>
Confirm passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase2' id='passphrase2'
size='64' type='password'
disabled={disableForm}
/>
</div>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Export'
disabled={disableForm}
/>
<AccessibleButton element='button' onClick={this.props.onFinished}
disabled={disableForm}>
Cancel
</AccessibleButton>
</div>
</form>
</BaseDialog>
);
},
});
170 changes: 170 additions & 0 deletions src/async-components/views/dialogs/ImportE2eKeysDialog.js
@@ -0,0 +1,170 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index';

function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.onerror = reject;

reader.readAsArrayBuffer(file);
});
}

const PHASE_EDIT = 1;
const PHASE_IMPORTING = 2;

export default React.createClass({
displayName: 'ImportE2eKeysDialog',

propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired,
},

getInitialState: function() {
return {
enableSubmit: false,
phase: PHASE_EDIT,
errStr: null,
};
},

componentWillMount: function() {
this._unmounted = false;
},

componentWillUnmount: function() {
this._unmounted = true;
},

_onFormChange: function(ev) {
const files = this.refs.file.files || [];
this.setState({
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
});
},

_onFormSubmit: function(ev) {
ev.preventDefault();
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
return false;
},

_startImport: function(file, passphrase) {
this.setState({
errStr: null,
phase: PHASE_IMPORTING,
});

return readFileAsArrayBuffer(file).then((arrayBuffer) => {
return MegolmExportEncryption.decryptMegolmKeyFile(
arrayBuffer, passphrase
);
}).then((keys) => {
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
}).then(() => {
// TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true);
}).catch((e) => {
if (this._unmounted) {
return;
}
this.setState({
errStr: e.message,
phase: PHASE_EDIT,
});
});
},

render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');

const disableForm = (this.state.phase !== PHASE_EDIT);

return (
<BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished}
title="Import room keys"
>
<form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content">
<p>
This process allows you to import encryption keys
that you had previously exported from another Matrix
client. You will then be able to decrypt any
messages that the other client could decrypt.
</p>
<p>
The export file will be protected with a passphrase.
You should enter the passphrase here, to decrypt the
file.
</p>
<div className='error'>
{this.state.errStr}
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
File to import
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='file' id='importFile' type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'>
Enter passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase' id='passphrase'
size='64' type='password'
onChange={this._onFormChange}
disabled={disableForm}/>
</div>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Import'
disabled={!this.state.enableSubmit || disableForm}
/>
<AccessibleButton element='button' onClick={this.props.onFinished}
disabled={disableForm}>
Cancel
</AccessibleButton>
</div>
</form>
</BaseDialog>
);
},
});

0 comments on commit dc7a169

Please sign in to comment.