From e23deac1bb52d644c5b40149943685b92c321d04 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 20 Jan 2017 15:12:50 +0000 Subject: [PATCH 1/2] Implement e2e export --- package.json | 3 +- .../views/dialogs/ExportE2eKeysDialog.js | 175 +++++++++++++----- src/components/structures/UserSettings.js | 21 +++ src/index.js | 16 +- 4 files changed, 156 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 0a8c09f984..dabac0a060 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 284d299f4b..816b8eb73d 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -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 = ( -
-

- 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. -

-

- 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. -

-
-
- -
-
- -
-
- -
-
-
+ _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 ( -
-
- Export room keys -
- {content} -
+ +
+
+

+ 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. +

+

+ 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. +

+
+ {this.state.errStr} +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + + Cancel + +
+
+
); }, }); diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ba5ab49bbc..6edf572f6c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -393,6 +393,16 @@ module.exports = React.createClass({ }).done(); }, + _onExportE2eKeysClicked: function() { + Modal.createDialogAsync( + (cb) => { + require(['../../async-components/views/dialogs/ExportE2eKeysDialog'], cb); + }, { + matrixClient: MatrixClientPeg.get(), + } + ); + }, + _renderUserInterfaceSettings: function() { var client = MatrixClientPeg.get(); @@ -463,6 +473,16 @@ module.exports = React.createClass({ const deviceId = client.deviceId; const identityKey = client.getDeviceEd25519Key() || ""; + let exportButton = null; + + if (client.isCryptoEnabled) { + exportButton = ( + + Export E2E room keys + + ); + } return (

Cryptography

@@ -471,6 +491,7 @@ module.exports = React.createClass({
  • {deviceId}
  • {identityKey}
  • + {exportButton}
    ); diff --git a/src/index.js b/src/index.js index 5d4145a39b..0e3e90aed6 100644 --- a/src/index.js +++ b/src/index.js @@ -29,20 +29,7 @@ module.exports.getComponent = function(componentName) { }; -/* hacky functions for megolm import/export until we give it a UI */ -import * as MegolmExportEncryption from './utils/MegolmExportEncryption'; -import MatrixClientPeg from './MatrixClientPeg'; - -window.exportKeys = function(password) { - return MatrixClientPeg.get().exportRoomKeys().then((k) => { - return MegolmExportEncryption.encryptMegolmKeyFile( - JSON.stringify(k), password - ); - }).then((f) => { - console.log(new TextDecoder().decode(new Uint8Array(f))); - }).done(); -}; - +/* window.importKeys = function(password, data) { const arrayBuffer = new TextEncoder().encode(data).buffer; return MegolmExportEncryption.decryptMegolmKeyFile( @@ -52,3 +39,4 @@ window.importKeys = function(password, data) { return MatrixClientPeg.get().importRoomKeys(k); }); }; +*/ From b85f53cadd6e6d3c97feefe8b54e11d3f62f4130 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 24 Jan 2017 15:57:42 +0000 Subject: [PATCH 2/2] Implement Megolm key importing --- .../views/dialogs/ImportE2eKeysDialog.js | 170 ++++++++++++++++++ src/components/structures/UserSettings.js | 26 ++- src/index.js | 13 -- 3 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 src/async-components/views/dialogs/ImportE2eKeysDialog.js diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js new file mode 100644 index 0000000000..586bd9b6cc --- /dev/null +++ b/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 ( + +
    +
    +

    + 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. +

    +

    + The export file will be protected with a passphrase. + You should enter the passphrase here, to decrypt the + file. +

    +
    + {this.state.errStr} +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + + Cancel + +
    +
    +
    + ); + }, +}); diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6edf572f6c..d64f0383f6 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -396,7 +396,21 @@ module.exports = React.createClass({ _onExportE2eKeysClicked: function() { Modal.createDialogAsync( (cb) => { - require(['../../async-components/views/dialogs/ExportE2eKeysDialog'], cb); + require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + } + ); + }, + + _onImportE2eKeysClicked: function() { + Modal.createDialogAsync( + (cb) => { + require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); + }, "e2e-export"); }, { matrixClient: MatrixClientPeg.get(), } @@ -473,7 +487,8 @@ module.exports = React.createClass({ const deviceId = client.deviceId; const identityKey = client.getDeviceEd25519Key() || ""; - let exportButton = null; + let exportButton = null, + importButton = null; if (client.isCryptoEnabled) { exportButton = ( @@ -482,6 +497,12 @@ module.exports = React.createClass({ Export E2E room keys ); + importButton = ( + + Import E2E room keys + + ); } return (
    @@ -492,6 +513,7 @@ module.exports = React.createClass({
  • {identityKey}
  • {exportButton} + {importButton}
    ); diff --git a/src/index.js b/src/index.js index 0e3e90aed6..b6d8c0b5f4 100644 --- a/src/index.js +++ b/src/index.js @@ -27,16 +27,3 @@ module.exports.resetSkin = function() { module.exports.getComponent = function(componentName) { return Skinner.getComponent(componentName); }; - - -/* -window.importKeys = function(password, data) { - const arrayBuffer = new TextEncoder().encode(data).buffer; - return MegolmExportEncryption.decryptMegolmKeyFile( - arrayBuffer, password - ).then((j) => { - const k = JSON.parse(j); - return MatrixClientPeg.get().importRoomKeys(k); - }); -}; -*/