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 =
);
},
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?
{_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."
+ )}
+
+ );
+ },
+});
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
;
+ }
+ }
+}
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'),