diff --git a/res/css/_components.scss b/res/css/_components.scss index 0e40b40a29..e8a8877d62 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -33,6 +33,7 @@ @import "./views/dialogs/_ChatInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; +@import "./views/dialogs/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/_CreateKeyBackupDialog.scss new file mode 100644 index 0000000000..a422cf858c --- /dev/null +++ b/res/css/views/dialogs/_CreateKeyBackupDialog.scss @@ -0,0 +1,25 @@ +/* +Copyright 2018 New Vector 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. +*/ + +.mx_CreateKeyBackupDialog { + padding-right: 40px; +} + +.mx_CreateKeyBackupDialog_recoveryKey { + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 39e973e8f7..900cd57b90 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +import SuggestKeyRestoreHandler from "../../SuggestKeyRestoreHandler"; + /** constants for MatrixChat.state.view */ const VIEWS = { // a special initial state which is only used at startup, while we are diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 53e1ddea71..b5cbf5bd89 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -736,6 +736,16 @@ module.exports = React.createClass({ ); } + + let keyBackupSection; + if (SettingsStore.isFeatureEnabled("feature_keybackup")) { + const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel'); + keyBackupSection =
+

{ _t("Key Backup") }

+ +
; + } + return (

{ _t("Cryptography") }

@@ -751,6 +761,7 @@ module.exports = React.createClass({
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
+ {keyBackupSection}
); }, diff --git a/src/components/views/dialogs/SuggestKeyBackupDialog.js b/src/components/views/dialogs/SuggestKeyBackupDialog.js deleted file mode 100644 index c2d6cfc60f..0000000000 --- a/src/components/views/dialogs/SuggestKeyBackupDialog.js +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2018 New Vector 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 Modal from '../../../Modal'; -import React from 'react'; -import PropTypes from 'prop-types'; -import sdk from '../../../index'; - -import { _t, _td } from '../../../languageHandler'; - -/** - * Dialog which asks the user whether they want to restore megolm keys - * from various sources when they first start using E2E on a new device. - */ -export default React.createClass({ - propTypes: { - onStartNewBackup: PropTypes.func.isRequired, - }, - - render: function() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - - return ( - -
-

To avoid ever losing your encrypted message history, you - can save your encryption keys on the server, protected by a recovery key. -

-

To maximise security, your recovery key is never stored by the app, - so you must store it yourself somewhere safe.

-

-

Warning: storing your encryption keys on the server means that - if someone gains access to your account and also steals your recovery key, - they will be able to read all of your encrypted conversation history. -

- -

Do you wish to generate a recovery key and backup your encryption - keys on the server? - -

- - -
-
-
- ); - }, -}); diff --git a/src/components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/components/views/dialogs/keybackup/CreateKeyBackupDialog.js new file mode 100644 index 0000000000..03410f4f7d --- /dev/null +++ b/src/components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -0,0 +1,230 @@ +/* +Copyright 2018 New Vector 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 Modal from '../../../../Modal'; +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../../index'; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import { formatCryptoKey } from '../../../../utils/FormattingUtils'; +import Promise from 'bluebird'; + +import { _t, _td } from '../../../../languageHandler'; + +const PHASE_INTRO = 0; +const PHASE_GENERATING = 1; +const PHASE_SHOWKEY = 2; +const PHASE_MAKEBACKUP = 3; +const PHASE_UPLOAD = 4; +const PHASE_DONE = 5; + +// XXX: copied from ShareDialog: factor out into utils +function selectText(target) { + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + +/** + * Walks the user through the process of creating an e22 key backup + * on the server. + */ +export default React.createClass({ + getInitialState: function() { + return { + phase: PHASE_INTRO, + }; + }, + + componentWillMount: function() { + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + }, + + _collectRecoveryKeyNode: function(n) { + this._recoveryKeyNode = n; + }, + + _copyRecoveryKey: function() { + selectText(this._recoveryKeyNode); + const successful = document.execCommand('copy'); + if (successful) { + this.setState({copied: true}); + } + }, + + _createBackup: function() { + this.setState({ + phase: PHASE_MAKEBACKUP, + error: null, + }); + this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ).then((info) => { + this.setState({ + phase: PHASE_UPLOAD, + }); + return MatrixClientPeg.get().backupAllGroupSessions(info.version); + }).then(() => { + this.setState({ + phase: PHASE_DONE, + }); + }).catch(e => { + console.log("Error creating key backup", e); + this.setState({ + error: e, + }); + }); + }, + + _onCancel: function() { + this.props.onFinished(false); + }, + + _onDone: function() { + this.props.onFinished(true); + }, + + _generateKey: async function() { + this.setState({ + phase: PHASE_GENERATING, + }); + // Look, work is being done! + await Promise.delay(1200); + this._keyBackupInfo = MatrixClientPeg.get().prepareKeyBackupVersion(); + this.setState({ + phase: PHASE_SHOWKEY, + }); + }, + + _renderPhaseIntro: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

To avoid ever losing your encrypted message history, you + can save your encryption keys on the server, protected by a recovery key. +

+

To maximise security, your recovery key is never stored by the app, + so you must store it yourself somewhere safe.

+

Warning: storing your encryption keys on the server means that + if someone gains access to your account and also steals your recovery key, + they will be able to read all of your encrypted conversation history. +

+ +

Do you wish to generate a recovery key and backup your encryption + keys on the server?

+ + +
; + }, + + _renderPhaseShowKey: function() { + return
+

{_t("Here is your recovery key:")}

+

+ {formatCryptoKey(this._keyBackupInfo.recovery_key)} +

+

{_t("This key can decrypt your full message history.")}

+

{_t( + "When you've saved it somewhere safe, proceed to the next step where the key will be used to "+ + "create an encrypted backup of your message keys and then destroyed." + )}

+
+ + +
+
; + }, + + _renderBusyPhase: function(text) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return
+

{_t(text)}

+ +
; + }, + + _renderPhaseDone: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t("Backup created")}

+

{_t("Your encryption keys are now being backed up to your Homeserver.")}

+ +
; + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + let content; + if (this.state.error) { + content =
+

{_t("Unable to create key backup")}

+
+ +
+
; + } else { + switch (this.state.phase) { + case PHASE_INTRO: + content = this._renderPhaseIntro(); + break; + case PHASE_GENERATING: + content = this._renderBusyPhase(_td("Generating recovery key...")); + break; + case PHASE_SHOWKEY: + content = this._renderPhaseShowKey(); + break; + case PHASE_MAKEBACKUP: + content = this._renderBusyPhase(_td("Creating backup...")); + break; + case PHASE_UPLOAD: + content = this._renderBusyPhase(_td("Uploading keys...")); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; + } + } + + return ( + +
+ {content} +
+
+ ); + }, +}); diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js new file mode 100644 index 0000000000..3b452e77f8 --- /dev/null +++ b/src/components/views/settings/KeyBackupPanel.js @@ -0,0 +1,134 @@ +/* +Copyright 2018 New Vector 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 PropTypes from 'prop-types'; + +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; + +export default class KeyBackupPanel extends React.Component { + constructor(props) { + super(props); + + this._startNewBackup = this._startNewBackup.bind(this); + this._deleteBackup = this._deleteBackup.bind(this); + + this._unmounted = false; + this.state = { + loading: true, + error: null, + backupInfo: null, + }; + this._loadBackupStatus(); + } + + componentWillUnmount() { + this._unmounted = true; + } + + async _loadBackupStatus() { + this.setState({loading: true}); + try { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + if (this._unmounted) return; + this.setState({ + backupInfo, + loading: false, + }); + } catch (e) { + console.log("Unable to fetch key backup status", e); + if (this._unmounted) return; + this.setState({ + error: e, + loading: false, + }); + return; + } + } + + _startNewBackup() { + const CreateKeyBackupDialog = sdk.getComponent("dialogs.keybackup.CreateKeyBackupDialog"); + Modal.createTrackedDialog('Key Backup', 'Key Backup', CreateKeyBackupDialog, { + onFinished: () => { + this._loadBackupStatus(); + }, + }); + } + + _deleteBackup() { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { + title: _t("Delete Backup"), + description: _t( + "Delete your backed up encryption keys from the server? " + + "You will no longer be able to use your recovery key to read encrypted message history" + ), + button: _t('Delete backup'), + danger: true, + onFinished: (proceed) => { + if (!proceed) return; + this.setState({loading: true}); + MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => { + this._loadBackupStatus(); + }); + }, + }); + + } + + render() { + const Spinner = sdk.getComponent("elements.Spinner"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + if (this.state.error) { + return ( +
+ {_t("Unable to load key backup status")} +
+ ); + } else if (this.state.loading) { + return ; + } else if (this.state.backupInfo) { + let clientBackupStatus; + if (MatrixClientPeg.get().getKeyBackupEnabled()) { + clientBackupStatus = _t("This device is uploading keys to this backup"); + } else { + // XXX: display why and how to fix it + clientBackupStatus = _t("This device is not uploading keys to this backup", {}, {b: x => {x}}); + } + return
+ {_t("Backup version: ")}{this.state.backupInfo.version}
+ {_t("Algorithm: ")}{this.state.backupInfo.algorithm}
+ {clientBackupStatus}

+ + { _t("Delete backup") } + +
; + } else { + return
+ {_t("No backup is present")}

+ + { _t("Start a new backup") } + +
; + } + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4643d4bdff..16695c8ec8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -216,6 +216,7 @@ "Failed to join room": "Failed to join room", "Message Pinning": "Message Pinning", "Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view", + "Backup of encryption keys to server": "Backup of encryption keys to server", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Hide removed messages": "Hide removed messages", @@ -297,6 +298,16 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", + "Delete Backup": "Delete Backup", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history", + "Delete backup": "Delete backup", + "Unable to load key backup status": "Unable to load key backup status", + "This device is uploading keys to this backup": "This device is uploading keys to this backup", + "This device is not uploading keys to this backup": "This device is not uploading keys to this backup", + "Backup version: ": "Backup version: ", + "Algorithm: ": "Algorithm: ", + "No backup is present": "No backup is present", + "Start a new backup": "Start a new backup", "Error saving email notification preferences": "Error saving email notification preferences", "An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.", "Keywords": "Keywords", @@ -931,6 +942,27 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", + "Generate recovery key": "Generate recovery key", + "I'll stick to manual backups": "I'll stick to manual backups", + "Here is your recovery key:": "Here is your recovery key:", + "This key can decrypt your full message history.": "This key can decrypt your full message history.", + "When you've saved it somewhere safe, proceed to the next step where the key will be used to create an encrypted backup of your message keys and then destroyed.": "When you've saved it somewhere safe, proceed to the next step where the key will be used to create an encrypted backup of your message keys and then destroyed.", + "Copy to clipboard": "Copy to clipboard", + "Proceed": "Proceed", + "Backup created": "Backup created", + "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", + "Unable to create key backup": "Unable to create key backup", + "Retry": "Retry", + "Generating recovery key...": "Generating recovery key...", + "Creating backup...": "Creating backup...", + "Uploading keys...": "Uploading keys...", + "Create Key Backup": "Create Key Backup", + "Backup encryption keys on your server?": "Backup encryption keys on your server?", + "Generate recovery key and enable online backups": "Generate recovery key and enable online backups", + "Restore encryption keys": "Restore encryption keys", + "Verify this device": "Verify this device", + "Restore from online backup": "Restore from online backup", + "Restore from offline backup": "Restore from offline backup", "Private Chat": "Private Chat", "Public Chat": "Public Chat", "Custom": "Custom", @@ -1124,6 +1156,7 @@ "Autocomplete Delay (ms):": "Autocomplete Delay (ms):", "": "", "Import E2E room keys": "Import E2E room keys", + "Key Backup": "Key Backup", "Cryptography": "Cryptography", "Device ID:": "Device ID:", "Device key:": "Device key:", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 0594c63eb9..3e0c374c8a 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -90,6 +90,12 @@ export const SETTINGS = { controller: new LazyLoadingController(), default: false, }, + "feature_keybackup": { + isFeature: true, + displayName: _td("Backup of encryption keys to server"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.dontSuggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Disable Emoji suggestions while typing'),