mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge pull request #2169 from matrix-org/dbkr/e2e_backups
Online incremental megolm backups (v2)
This commit is contained in:
		
						commit
						d714176fcd
					
				@ -262,6 +262,11 @@ textarea {
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_linkButton {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: $accent-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_Dialog_title {
 | 
			
		||||
    min-height: 16px;
 | 
			
		||||
    padding-top: 40px;
 | 
			
		||||
 | 
			
		||||
@ -33,17 +33,21 @@
 | 
			
		||||
@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";
 | 
			
		||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_GroupAddressPicker.scss";
 | 
			
		||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_SetEmailDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_SetMxIdDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_SetPasswordDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_ShareDialog.scss";
 | 
			
		||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
 | 
			
		||||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
 | 
			
		||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
 | 
			
		||||
@import "./views/directory/_NetworkDropdown.scss";
 | 
			
		||||
@import "./views/elements/_AccessibleButton.scss";
 | 
			
		||||
@import "./views/elements/_AddressSelector.scss";
 | 
			
		||||
@ -107,6 +111,7 @@
 | 
			
		||||
@import "./views/rooms/_TopUnreadMessagesBar.scss";
 | 
			
		||||
@import "./views/settings/_DevicesPanel.scss";
 | 
			
		||||
@import "./views/settings/_IntegrationsManager.scss";
 | 
			
		||||
@import "./views/settings/_KeyBackupPanel.scss";
 | 
			
		||||
@import "./views/settings/_Notifications.scss";
 | 
			
		||||
@import "./views/voip/_CallView.scss";
 | 
			
		||||
@import "./views/voip/_IncomingCallbox.scss";
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								res/css/views/dialogs/_CreateKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								res/css/views/dialogs/_CreateKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								res/css/views/dialogs/_RestoreKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								res/css/views/dialogs/_RestoreKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
/*
 | 
			
		||||
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_RestoreKeyBackupDialog_keyStatus {
 | 
			
		||||
    height: 30px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
/*
 | 
			
		||||
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_primaryContainer {
 | 
			
		||||
    /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/
 | 
			
		||||
    padding: 20px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_passPhraseInput {
 | 
			
		||||
    width: 300px;
 | 
			
		||||
    border: 1px solid $accent-color;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_passPhraseMatch {
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_recoveryKey {
 | 
			
		||||
    width: 300px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
/*
 | 
			
		||||
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_RestoreKeyBackupDialog_primaryContainer {
 | 
			
		||||
    /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/
 | 
			
		||||
    padding: 20px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_RestoreKeyBackupDialog_passPhraseInput,
 | 
			
		||||
.mx_RestoreKeyBackupDialog_recoveryKeyInput {
 | 
			
		||||
    width: 300px;
 | 
			
		||||
    border: 1px solid $accent-color;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								res/css/views/settings/_KeyBackupPanel.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								res/css/views/settings/_KeyBackupPanel.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
/*
 | 
			
		||||
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_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_sigInvalid,
 | 
			
		||||
.mx_KeyBackupPanel_deviceVerified, .mx_KeyBackupPanel_deviceNotVerified {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_deviceVerified {
 | 
			
		||||
    color: $e2e-verified-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_KeyBackupPanel_sigInvalid, .mx_KeyBackupPanel_deviceNotVerified {
 | 
			
		||||
    color: $e2e-warning-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_KeyBackupPanel_deviceName {
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
@ -1375,6 +1375,7 @@ export default React.createClass({
 | 
			
		||||
        cli.on("crypto.roomKeyRequestCancellation", (req) => {
 | 
			
		||||
            krh.handleKeyRequestCancellation(req);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cli.on("Room", (room) => {
 | 
			
		||||
            if (MatrixClientPeg.get().isCryptoEnabled()) {
 | 
			
		||||
                const blacklistEnabled = SettingsStore.getValueAt(
 | 
			
		||||
 | 
			
		||||
@ -737,6 +737,16 @@ module.exports = React.createClass({
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let keyBackupSection;
 | 
			
		||||
        if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
 | 
			
		||||
            const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
 | 
			
		||||
            keyBackupSection = <div className="mx_UserSettings_section">
 | 
			
		||||
                <h3>{ _t("Key Backup") }</h3>
 | 
			
		||||
                <KeyBackupPanel />
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div>
 | 
			
		||||
                <h3>{ _t("Cryptography") }</h3>
 | 
			
		||||
@ -752,6 +762,7 @@ module.exports = React.createClass({
 | 
			
		||||
                <div className="mx_UserSettings_section">
 | 
			
		||||
                    { CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
 | 
			
		||||
                </div>
 | 
			
		||||
                {keyBackupSection}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										460
									
								
								src/components/views/dialogs/keybackup/CreateKeyBackupDialog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								src/components/views/dialogs/keybackup/CreateKeyBackupDialog.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,460 @@
 | 
			
		||||
/*
 | 
			
		||||
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 sdk from '../../../../index';
 | 
			
		||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
 | 
			
		||||
 | 
			
		||||
import FileSaver from 'file-saver';
 | 
			
		||||
 | 
			
		||||
import { _t, _td } from '../../../../languageHandler';
 | 
			
		||||
 | 
			
		||||
const PHASE_PASSPHRASE = 0;
 | 
			
		||||
const PHASE_PASSPHRASE_CONFIRM = 1;
 | 
			
		||||
const PHASE_SHOWKEY = 2;
 | 
			
		||||
const PHASE_KEEPITSAFE = 3;
 | 
			
		||||
const PHASE_BACKINGUP = 4;
 | 
			
		||||
const PHASE_DONE = 5;
 | 
			
		||||
const PHASE_OPTOUT_CONFIRM = 6;
 | 
			
		||||
 | 
			
		||||
// 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 e2e key backup
 | 
			
		||||
 * on the server.
 | 
			
		||||
 */
 | 
			
		||||
export default React.createClass({
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
        return {
 | 
			
		||||
            phase: PHASE_PASSPHRASE,
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            copied: false,
 | 
			
		||||
            downloaded: false,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    componentWillMount: function() {
 | 
			
		||||
        this._recoveryKeyNode = null;
 | 
			
		||||
        this._keyBackupInfo = null;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _collectRecoveryKeyNode: function(n) {
 | 
			
		||||
        this._recoveryKeyNode = n;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onCopyClick: function() {
 | 
			
		||||
        selectText(this._recoveryKeyNode);
 | 
			
		||||
        const successful = document.execCommand('copy');
 | 
			
		||||
        if (successful) {
 | 
			
		||||
            this.setState({
 | 
			
		||||
                copied: true,
 | 
			
		||||
                phase: PHASE_KEEPITSAFE,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onDownloadClick: function() {
 | 
			
		||||
        const blob = new Blob([this._keyBackupInfo.recovery_key], {
 | 
			
		||||
            type: 'text/plain;charset=us-ascii',
 | 
			
		||||
        });
 | 
			
		||||
        FileSaver.saveAs(blob, 'recovery-key.txt');
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            downloaded: true,
 | 
			
		||||
            phase: PHASE_KEEPITSAFE,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _createBackup: function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            phase: PHASE_BACKINGUP,
 | 
			
		||||
            error: null,
 | 
			
		||||
        });
 | 
			
		||||
        this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion(
 | 
			
		||||
            this._keyBackupInfo,
 | 
			
		||||
        ).then((info) => {
 | 
			
		||||
            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);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onOptOutClick: function() {
 | 
			
		||||
        this.setState({phase: PHASE_OPTOUT_CONFIRM});
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onSetUpClick: function() {
 | 
			
		||||
        this.setState({phase: PHASE_PASSPHRASE});
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onSkipPassPhraseClick: async function() {
 | 
			
		||||
        this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
 | 
			
		||||
        this.setState({
 | 
			
		||||
            copied: false,
 | 
			
		||||
            phase: PHASE_SHOWKEY,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseNextClick: function() {
 | 
			
		||||
        this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseKeyPress: function(e) {
 | 
			
		||||
        if (e.key === 'Enter' && this._passPhraseIsValid()) {
 | 
			
		||||
            this._onPassPhraseNextClick();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmNextClick: async function() {
 | 
			
		||||
        this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
 | 
			
		||||
        this.setState({
 | 
			
		||||
            copied: false,
 | 
			
		||||
            phase: PHASE_SHOWKEY,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmKeyPress: function(e) {
 | 
			
		||||
        if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
 | 
			
		||||
            this._onPassPhraseConfirmNextClick();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onSetAgainClick: function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            phase: PHASE_PASSPHRASE,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onKeepItSafeGotItClick: function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            phase: PHASE_SHOWKEY,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseChange: function(e) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: e.target.value,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmChange: function(e) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhraseConfirm: e.target.value,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _passPhraseIsValid: function() {
 | 
			
		||||
        return this.state.passPhrase !== '';
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhasePassPhrase: function() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
			
		||||
        return <div>
 | 
			
		||||
            <p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p>
 | 
			
		||||
            <p>{_t("You'll need it if you log out or lose access to this device.")}</p>
 | 
			
		||||
 | 
			
		||||
            <div className="mx_CreateKeyBackupDialog_primaryContainer">
 | 
			
		||||
                <input type="password"
 | 
			
		||||
                    onChange={this._onPassPhraseChange}
 | 
			
		||||
                    onKeyPress={this._onPassPhraseKeyPress}
 | 
			
		||||
                    value={this.state.passPhrase}
 | 
			
		||||
                    className="mx_CreateKeyBackupDialog_passPhraseInput"
 | 
			
		||||
                    placeholder={_t("Enter a passphrase...")}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <DialogButtons primaryButton={_t('Next')}
 | 
			
		||||
                onPrimaryButtonClick={this._onPassPhraseNextClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
                disabled={!this._passPhraseIsValid()}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <p>{_t(
 | 
			
		||||
                "If you don't want encrypted message history to be availble on other devices, "+
 | 
			
		||||
                "<button>opt out</button>.",
 | 
			
		||||
                {},
 | 
			
		||||
                {
 | 
			
		||||
                    button: sub => <AccessibleButton
 | 
			
		||||
                        element="span"
 | 
			
		||||
                        className="mx_linkButton"
 | 
			
		||||
                        onClick={this._onOptOutClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {sub}
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                },
 | 
			
		||||
            )}</p>
 | 
			
		||||
            <p>{_t(
 | 
			
		||||
                "Or, if you don't want to create a Recovery Passphrase, skip this step and "+
 | 
			
		||||
                "<button>download a recovery key</button>.",
 | 
			
		||||
                {},
 | 
			
		||||
                {
 | 
			
		||||
                    button: sub => <AccessibleButton
 | 
			
		||||
                        element="span"
 | 
			
		||||
                        className="mx_linkButton"
 | 
			
		||||
                        onClick={this._onSkipPassPhraseClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {sub}
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                },
 | 
			
		||||
            )}</p>
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhasePassPhraseConfirm: function() {
 | 
			
		||||
        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
			
		||||
 | 
			
		||||
        let passPhraseMatch = null;
 | 
			
		||||
        if (this.state.passPhraseConfirm.length > 0) {
 | 
			
		||||
            let matchText;
 | 
			
		||||
            if (this.state.passPhraseConfirm === this.state.passPhrase) {
 | 
			
		||||
                matchText = _t("That matches!");
 | 
			
		||||
            } else {
 | 
			
		||||
                matchText = _t("That doesn't match.");
 | 
			
		||||
            }
 | 
			
		||||
            passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
 | 
			
		||||
                <div>{matchText}</div>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
 | 
			
		||||
                        {_t("Go back to set it again.")}
 | 
			
		||||
                    </AccessibleButton>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        return <div>
 | 
			
		||||
            <p>{_t(
 | 
			
		||||
                "Type in your Recovery Passphrase to confirm you remember it. " +
 | 
			
		||||
                "If it helps, add it to your password manager or store it " +
 | 
			
		||||
                "somewhere safe.",
 | 
			
		||||
            )}</p>
 | 
			
		||||
            <div className="mx_CreateKeyBackupDialog_primaryContainer">
 | 
			
		||||
                {passPhraseMatch}
 | 
			
		||||
                <div>
 | 
			
		||||
                    <input type="password"
 | 
			
		||||
                        onChange={this._onPassPhraseConfirmChange}
 | 
			
		||||
                        onKeyPress={this._onPassPhraseConfirmKeyPress}
 | 
			
		||||
                        value={this.state.passPhraseConfirm}
 | 
			
		||||
                        className="mx_CreateKeyBackupDialog_passPhraseInput"
 | 
			
		||||
                        placeholder={_t("Repeat your passphrase...")}
 | 
			
		||||
                        autoFocus={true}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <DialogButtons primaryButton={_t('Next')}
 | 
			
		||||
                onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
                disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
 | 
			
		||||
            />
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhaseShowKey: function() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        return <div>
 | 
			
		||||
            <p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
 | 
			
		||||
            <p>{_t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.")}</p>
 | 
			
		||||
            <p className="mx_CreateKeyBackupDialog_primaryContainer">
 | 
			
		||||
                <div>{_t("Your Recovery Key")}</div>
 | 
			
		||||
                <div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
 | 
			
		||||
                    <button onClick={this._onCopyClick}>
 | 
			
		||||
                        {_t("Copy to clipboard")}
 | 
			
		||||
                    </button>
 | 
			
		||||
                    {
 | 
			
		||||
                        // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design
 | 
			
		||||
                    }
 | 
			
		||||
                    <br /><br />
 | 
			
		||||
                    <button onClick={this._onDownloadClick}>
 | 
			
		||||
                        {_t("Download")}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="mx_CreateKeyBackupDialog_recoveryKey">
 | 
			
		||||
                    <code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
 | 
			
		||||
                </div>
 | 
			
		||||
            </p>
 | 
			
		||||
            <br />
 | 
			
		||||
            <DialogButtons primaryButton={_t("I've made a copy")}
 | 
			
		||||
                onPrimaryButtonClick={this._createBackup}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
                disabled={!this.state.copied}
 | 
			
		||||
            />
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhaseKeepItSafe: function() {
 | 
			
		||||
        let introText;
 | 
			
		||||
        if (this.state.copied) {
 | 
			
		||||
            introText = _t(
 | 
			
		||||
                "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
 | 
			
		||||
                {}, {b: s => <b>{s}</b>},
 | 
			
		||||
            );
 | 
			
		||||
        } else if (this.state.downloaded) {
 | 
			
		||||
            introText = _t(
 | 
			
		||||
                "Your Recovery Key is in your <b>Downloads</b> folder.",
 | 
			
		||||
                {}, {b: s => <b>{s}</b>},
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        return <div>
 | 
			
		||||
            {introText}
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
 | 
			
		||||
                <li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
 | 
			
		||||
                <li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
 | 
			
		||||
            </ul>
 | 
			
		||||
            <DialogButtons primaryButton={_t("Got it")}
 | 
			
		||||
                onPrimaryButtonClick={this._onKeepItSafeGotItClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
            />
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderBusyPhase: function(text) {
 | 
			
		||||
        const Spinner = sdk.getComponent('views.elements.Spinner');
 | 
			
		||||
        return <div>
 | 
			
		||||
            <p>{_t(text)}</p>
 | 
			
		||||
            <Spinner />
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhaseDone: function() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        return <div>
 | 
			
		||||
            <p>{_t("Backup created")}</p>
 | 
			
		||||
            <p>{_t("Your encryption keys are now being backed up to your Homeserver.")}</p>
 | 
			
		||||
            <DialogButtons primaryButton={_t('Close')}
 | 
			
		||||
                onPrimaryButtonClick={this._onDone}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
            />
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _renderPhaseOptOutConfirm: function() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        return <div>
 | 
			
		||||
            {_t(
 | 
			
		||||
                "Without setting up Secure Message Recovery, you won't be able to restore your " +
 | 
			
		||||
                "encrypted message history if you log out or use another device.",
 | 
			
		||||
            )}
 | 
			
		||||
            <DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
 | 
			
		||||
                onPrimaryButtonClick={this._onSetUpClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
            >
 | 
			
		||||
                <button onClick={this._onCancel}>I understand, continue without</button>
 | 
			
		||||
            </DialogButtons>
 | 
			
		||||
        </div>;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _titleForPhase: function(phase) {
 | 
			
		||||
        switch (phase) {
 | 
			
		||||
            case PHASE_PASSPHRASE:
 | 
			
		||||
                return _t('Create a Recovery Passphrase');
 | 
			
		||||
            case PHASE_PASSPHRASE_CONFIRM:
 | 
			
		||||
                return _t('Confirm Recovery Passphrase');
 | 
			
		||||
            case PHASE_OPTOUT_CONFIRM:
 | 
			
		||||
                return _t('Warning!');
 | 
			
		||||
            case PHASE_SHOWKEY:
 | 
			
		||||
                return _t('Recovery Key');
 | 
			
		||||
            case PHASE_KEEPITSAFE:
 | 
			
		||||
                return _t('Keep it safe');
 | 
			
		||||
            case PHASE_BACKINGUP:
 | 
			
		||||
                return _t('Backing up...');
 | 
			
		||||
            default:
 | 
			
		||||
                return _t("Create Key Backup");
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
 | 
			
		||||
 | 
			
		||||
        let content;
 | 
			
		||||
        if (this.state.error) {
 | 
			
		||||
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
            content = <div>
 | 
			
		||||
                <p>{_t("Unable to create key backup")}</p>
 | 
			
		||||
                <div className="mx_Dialog_buttons">
 | 
			
		||||
                    <DialogButtons primaryButton={_t('Retry')}
 | 
			
		||||
                        onPrimaryButtonClick={this._createBackup}
 | 
			
		||||
                        hasCancel={true}
 | 
			
		||||
                        onCancel={this._onCancel}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>;
 | 
			
		||||
        } else {
 | 
			
		||||
            switch (this.state.phase) {
 | 
			
		||||
                case PHASE_PASSPHRASE:
 | 
			
		||||
                    content = this._renderPhasePassPhrase();
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_PASSPHRASE_CONFIRM:
 | 
			
		||||
                    content = this._renderPhasePassPhraseConfirm();
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_SHOWKEY:
 | 
			
		||||
                    content = this._renderPhaseShowKey();
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_KEEPITSAFE:
 | 
			
		||||
                    content = this._renderPhaseKeepItSafe();
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_BACKINGUP:
 | 
			
		||||
                    content = this._renderBusyPhase(_td("Backing up..."));
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_DONE:
 | 
			
		||||
                    content = this._renderPhaseDone();
 | 
			
		||||
                    break;
 | 
			
		||||
                case PHASE_OPTOUT_CONFIRM:
 | 
			
		||||
                    content = this._renderPhaseOptOutConfirm();
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <BaseDialog className='mx_CreateKeyBackupDialog'
 | 
			
		||||
                onFinished={this.props.onFinished}
 | 
			
		||||
                title={this._titleForPhase(this.state.phase)}
 | 
			
		||||
                hasCancel={[PHASE_DONE].includes(this.state.phase)}
 | 
			
		||||
            >
 | 
			
		||||
            <div>
 | 
			
		||||
                {content}
 | 
			
		||||
            </div>
 | 
			
		||||
            </BaseDialog>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										303
									
								
								src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,303 @@
 | 
			
		||||
/*
 | 
			
		||||
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 sdk from '../../../../index';
 | 
			
		||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
 | 
			
		||||
import Modal from '../../../../Modal';
 | 
			
		||||
 | 
			
		||||
import { _t } from '../../../../languageHandler';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Dialog for restoring e2e keys from a backup and the user's recovery key
 | 
			
		||||
 */
 | 
			
		||||
export default React.createClass({
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
        return {
 | 
			
		||||
            backupInfo: null,
 | 
			
		||||
            loading: false,
 | 
			
		||||
            loadError: null,
 | 
			
		||||
            restoreError: null,
 | 
			
		||||
            recoveryKey: "",
 | 
			
		||||
            recoverInfo: null,
 | 
			
		||||
            recoveryKeyValid: false,
 | 
			
		||||
            forceRecoveryKey: false,
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    componentWillMount: function() {
 | 
			
		||||
        this._loadBackupStatus();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onCancel: function() {
 | 
			
		||||
        this.props.onFinished(false);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onDone: function() {
 | 
			
		||||
        this.props.onFinished(true);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onUseRecoveryKeyClick: function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            forceRecoveryKey: true,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onResetRecoveryClick: function() {
 | 
			
		||||
        this.props.onFinished(false);
 | 
			
		||||
        const CreateKeyBackupDialog = sdk.getComponent("dialogs.keybackup.CreateKeyBackupDialog");
 | 
			
		||||
        Modal.createTrackedDialog('Create Key Backup', '', CreateKeyBackupDialog, {});
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onRecoveryKeyChange: function(e) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            recoveryKey: e.target.value,
 | 
			
		||||
            recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseNext: async function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            loading: true,
 | 
			
		||||
            restoreError: null,
 | 
			
		||||
        });
 | 
			
		||||
        try {
 | 
			
		||||
            const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
 | 
			
		||||
                this.state.passPhrase, undefined, undefined, this.state.backupInfo.version,
 | 
			
		||||
            );
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loading: false,
 | 
			
		||||
                recoverInfo,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.log("Error restoring backup", e);
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loading: false,
 | 
			
		||||
                restoreError: e,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onRecoveryKeyNext: async function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            loading: true,
 | 
			
		||||
            restoreError: null,
 | 
			
		||||
        });
 | 
			
		||||
        try {
 | 
			
		||||
            const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
 | 
			
		||||
                this.state.recoveryKey, undefined, undefined, this.state.backupInfo.version,
 | 
			
		||||
            );
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loading: false,
 | 
			
		||||
                recoverInfo,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.log("Error restoring backup", e);
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loading: false,
 | 
			
		||||
                restoreError: e,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseChange: function(e) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: e.target.value,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseKeyPress: function(e) {
 | 
			
		||||
        if (e.key === "Enter") {
 | 
			
		||||
            this._onPassPhraseNext();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _onRecoveryKeyKeyPress: function(e) {
 | 
			
		||||
        if (e.key === "Enter" && this.state.recoveryKeyValid) {
 | 
			
		||||
            this._onRecoveryKeyNext();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _loadBackupStatus: async function() {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            loading: true,
 | 
			
		||||
            loadError: null,
 | 
			
		||||
        });
 | 
			
		||||
        try {
 | 
			
		||||
            const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loadError: null,
 | 
			
		||||
                loading: false,
 | 
			
		||||
                backupInfo,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.log("Error loading backup status", e);
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loadError: e,
 | 
			
		||||
                loading: false,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
 | 
			
		||||
        const Spinner = sdk.getComponent("elements.Spinner");
 | 
			
		||||
 | 
			
		||||
        const backupHasPassphrase = (
 | 
			
		||||
            this.state.backupInfo &&
 | 
			
		||||
            this.state.backupInfo.auth_data &&
 | 
			
		||||
            this.state.backupInfo.auth_data.private_key_salt &&
 | 
			
		||||
            this.state.backupInfo.auth_data.private_key_iterations
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let content;
 | 
			
		||||
        let title;
 | 
			
		||||
        if (this.state.loading) {
 | 
			
		||||
            title = _t("Loading...");
 | 
			
		||||
            content = <Spinner />;
 | 
			
		||||
        } else if (this.state.loadError) {
 | 
			
		||||
            title = _t("Error");
 | 
			
		||||
            content = _t("Unable to load backup status");
 | 
			
		||||
        } else if (this.state.restoreError) {
 | 
			
		||||
            title = _t("Error");
 | 
			
		||||
            content = _t("Unable to restore backup");
 | 
			
		||||
        } else if (this.state.backupInfo === null) {
 | 
			
		||||
            title = _t("Error");
 | 
			
		||||
            content = _t("No backup found!");
 | 
			
		||||
        } else if (this.state.recoverInfo) {
 | 
			
		||||
            title = _t("Backup Restored");
 | 
			
		||||
            let failedToDecrypt;
 | 
			
		||||
            if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
 | 
			
		||||
                failedToDecrypt = <p>{_t(
 | 
			
		||||
                    "Failed to decrypt %(failedCount)s sessions!",
 | 
			
		||||
                    {failedCount: this.state.recoverInfo.total - this.state.recoverInfo.imported},
 | 
			
		||||
                )}</p>;
 | 
			
		||||
            }
 | 
			
		||||
            content = <div>
 | 
			
		||||
                <p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
 | 
			
		||||
                {failedToDecrypt}
 | 
			
		||||
            </div>;
 | 
			
		||||
        } else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
 | 
			
		||||
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
            const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
			
		||||
            title = _t("Enter Recovery Passphrase");
 | 
			
		||||
            content = <div>
 | 
			
		||||
                {_t(
 | 
			
		||||
                    "Access your secure message history and set up secure " +
 | 
			
		||||
                    "messaging by entering your recovery passphrase.",
 | 
			
		||||
                )}<br />
 | 
			
		||||
 | 
			
		||||
                <div className="mx_RestoreKeyBackupDialog_primaryContainer">
 | 
			
		||||
                    <input type="password"
 | 
			
		||||
                        className="mx_RestoreKeyBackupDialog_passPhraseInput"
 | 
			
		||||
                        onChange={this._onPassPhraseChange}
 | 
			
		||||
                        onKeyPress={this._onPassPhraseKeyPress}
 | 
			
		||||
                        value={this.state.passPhrase}
 | 
			
		||||
                        autoFocus={true}
 | 
			
		||||
                    />
 | 
			
		||||
                    <DialogButtons primaryButton={_t('Next')}
 | 
			
		||||
                        onPrimaryButtonClick={this._onPassPhraseNext}
 | 
			
		||||
                        hasCancel={true}
 | 
			
		||||
                        onCancel={this._onCancel}
 | 
			
		||||
                        focus={false}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                {_t(
 | 
			
		||||
                    "If you've forgotten your recovery passphrase you can "+
 | 
			
		||||
                    "<button1>use your recovery key</button1> or " +
 | 
			
		||||
                    "<button2>set up new recovery options</button2>"
 | 
			
		||||
                , {}, {
 | 
			
		||||
                    button1: s => <AccessibleButton className="mx_linkButton"
 | 
			
		||||
                        element="span"
 | 
			
		||||
                        onClick={this._onUseRecoveryKeyClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {s}
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                    button2: s => <AccessibleButton className="mx_linkButton"
 | 
			
		||||
                        element="span"
 | 
			
		||||
                        onClick={this._onResetRecoveryClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {s}
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                })}
 | 
			
		||||
            </div>;
 | 
			
		||||
        } else {
 | 
			
		||||
            title = _t("Enter Recovery Key");
 | 
			
		||||
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
            const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
			
		||||
 | 
			
		||||
            let keyStatus;
 | 
			
		||||
            if (this.state.recoveryKey.length === 0) {
 | 
			
		||||
                keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
 | 
			
		||||
            } else if (this.state.recoveryKeyValid) {
 | 
			
		||||
                keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
 | 
			
		||||
                    {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
 | 
			
		||||
                </div>;
 | 
			
		||||
            } else {
 | 
			
		||||
                keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
 | 
			
		||||
                    {"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
 | 
			
		||||
                </div>;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            content = <div>
 | 
			
		||||
                {_t(
 | 
			
		||||
                    "Access your secure message history and set up secure " +
 | 
			
		||||
                    "messaging by entering your recovery key.",
 | 
			
		||||
                )}<br />
 | 
			
		||||
 | 
			
		||||
                <div className="mx_RestoreKeyBackupDialog_primaryContainer">
 | 
			
		||||
                    <input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
 | 
			
		||||
                        onChange={this._onRecoveryKeyChange}
 | 
			
		||||
                        onKeyPress={this._onRecoveryKeyKeyPress}
 | 
			
		||||
                        value={this.state.recoveryKey}
 | 
			
		||||
                        autoFocus={true}
 | 
			
		||||
                    />
 | 
			
		||||
                    {keyStatus}
 | 
			
		||||
                    <DialogButtons primaryButton={_t('Next')}
 | 
			
		||||
                        onPrimaryButtonClick={this._onRecoveryKeyNext}
 | 
			
		||||
                        hasCancel={true}
 | 
			
		||||
                        onCancel={this._onCancel}
 | 
			
		||||
                        focus={false}
 | 
			
		||||
                        primaryDisabled={!this.state.recoveryKeyValid}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                {_t(
 | 
			
		||||
                    "If you've forgotten your recovery passphrase you can "+
 | 
			
		||||
                    "<button>set up new recovery options</button>"
 | 
			
		||||
                , {}, {
 | 
			
		||||
                    button: s => <AccessibleButton className="mx_linkButton"
 | 
			
		||||
                        element="span"
 | 
			
		||||
                        onClick={this._onResetRecoveryClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {s}
 | 
			
		||||
                    </AccessibleButton>,
 | 
			
		||||
                })}
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <BaseDialog className='mx_RestoreKeyBackupDialog'
 | 
			
		||||
                onFinished={this.props.onFinished}
 | 
			
		||||
                title={title}
 | 
			
		||||
            >
 | 
			
		||||
            <div>
 | 
			
		||||
                {content}
 | 
			
		||||
            </div>
 | 
			
		||||
            </BaseDialog>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
@ -43,7 +43,11 @@ module.exports = React.createClass({
 | 
			
		||||
 | 
			
		||||
        focus: PropTypes.bool,
 | 
			
		||||
 | 
			
		||||
        // disables the primary and cancel buttons
 | 
			
		||||
        disabled: PropTypes.bool,
 | 
			
		||||
 | 
			
		||||
        // disables only the primary button
 | 
			
		||||
        primaryDisabled: PropTypes.bool,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getDefaultProps: function() {
 | 
			
		||||
@ -75,7 +79,7 @@ module.exports = React.createClass({
 | 
			
		||||
                <button className={primaryButtonClassName}
 | 
			
		||||
                    onClick={this.props.onPrimaryButtonClick}
 | 
			
		||||
                    autoFocus={this.props.focus}
 | 
			
		||||
                        disabled={this.props.disabled}
 | 
			
		||||
                    disabled={this.props.disabled || this.props.primaryDisabled}
 | 
			
		||||
                >
 | 
			
		||||
                    { this.props.primaryButton }
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										240
									
								
								src/components/views/settings/KeyBackupPanel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/components/views/settings/KeyBackupPanel.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,240 @@
 | 
			
		||||
/*
 | 
			
		||||
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 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._verifyDevice = this._verifyDevice.bind(this);
 | 
			
		||||
        this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
 | 
			
		||||
        this._restoreBackup = this._restoreBackup.bind(this);
 | 
			
		||||
 | 
			
		||||
        this._unmounted = false;
 | 
			
		||||
        this.state = {
 | 
			
		||||
            loading: true,
 | 
			
		||||
            error: null,
 | 
			
		||||
            backupInfo: null,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillMount() {
 | 
			
		||||
        this._loadBackupStatus();
 | 
			
		||||
 | 
			
		||||
        MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        this._unmounted = true;
 | 
			
		||||
 | 
			
		||||
        if (MatrixClientPeg.get()) {
 | 
			
		||||
            MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onKeyBackupStatus() {
 | 
			
		||||
        this._loadBackupStatus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _loadBackupStatus() {
 | 
			
		||||
        this.setState({loading: true});
 | 
			
		||||
        try {
 | 
			
		||||
            const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
 | 
			
		||||
            const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
 | 
			
		||||
            if (this._unmounted) return;
 | 
			
		||||
            this.setState({
 | 
			
		||||
                backupInfo,
 | 
			
		||||
                backupSigStatus,
 | 
			
		||||
                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();
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _restoreBackup() {
 | 
			
		||||
        const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
 | 
			
		||||
        Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _verifyDevice(e) {
 | 
			
		||||
        const device = this.state.backupSigStatus.sigs[e.target.getAttribute('data-sigindex')].device;
 | 
			
		||||
 | 
			
		||||
        const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
 | 
			
		||||
        Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
 | 
			
		||||
            userId: MatrixClientPeg.get().credentials.userId,
 | 
			
		||||
            device: device,
 | 
			
		||||
            onFinished: () => {
 | 
			
		||||
                this._loadBackupStatus();
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const Spinner = sdk.getComponent("elements.Spinner");
 | 
			
		||||
        const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
 | 
			
		||||
 | 
			
		||||
        if (this.state.error) {
 | 
			
		||||
            return (
 | 
			
		||||
                <div className="error">
 | 
			
		||||
                    {_t("Unable to load key backup status")}
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        } else if (this.state.loading) {
 | 
			
		||||
            return <Spinner />;
 | 
			
		||||
        } 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 <b>not</b> uploading keys to this backup", {},
 | 
			
		||||
                    {b: x => <b>{x}</b>},
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
 | 
			
		||||
                const sigStatusSubstitutions = {
 | 
			
		||||
                    validity: sub =>
 | 
			
		||||
                        <span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
 | 
			
		||||
                            {sub}
 | 
			
		||||
                        </span>,
 | 
			
		||||
                    verify: sub =>
 | 
			
		||||
                        <span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
 | 
			
		||||
                            {sub}
 | 
			
		||||
                        </span>,
 | 
			
		||||
                    device: sub => <span className="mx_KeyBackupPanel_deviceName">{sig.device.getDisplayName()}</span>,
 | 
			
		||||
                };
 | 
			
		||||
                let sigStatus;
 | 
			
		||||
                if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
 | 
			
		||||
                    sigStatus = _t(
 | 
			
		||||
                        "Backup has a <validity>valid</validity> signature from this device",
 | 
			
		||||
                        {}, sigStatusSubstitutions,
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (sig.valid && sig.device.isVerified()) {
 | 
			
		||||
                    sigStatus = _t(
 | 
			
		||||
                        "Backup has a <validity>valid</validity> signature from " +
 | 
			
		||||
                        "<verify>verified</verify> device <device>x</device>",
 | 
			
		||||
                        {}, sigStatusSubstitutions,
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (sig.valid && !sig.device.isVerified()) {
 | 
			
		||||
                    sigStatus = _t(
 | 
			
		||||
                        "Backup has a <validity>valid</validity> signature from " +
 | 
			
		||||
                        "<verify>unverified</verify> device <device></device>",
 | 
			
		||||
                        {}, sigStatusSubstitutions,
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (!sig.valid && sig.device.isVerified()) {
 | 
			
		||||
                    sigStatus = _t(
 | 
			
		||||
                        "Backup has an <validity>invalid</validity> signature from " +
 | 
			
		||||
                        "<verify>verified</verify> device <device></device>",
 | 
			
		||||
                        {}, sigStatusSubstitutions,
 | 
			
		||||
                    );
 | 
			
		||||
                } else if (!sig.valid && !sig.device.isVerified()) {
 | 
			
		||||
                    sigStatus = _t(
 | 
			
		||||
                        "Backup has an <validity>invalid</validity> signature from " +
 | 
			
		||||
                        "<verify>unverified</verify> device <device></device>",
 | 
			
		||||
                        {}, sigStatusSubstitutions,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let verifyButton;
 | 
			
		||||
                if (!sig.device.isVerified()) {
 | 
			
		||||
                    verifyButton = <div><br /><AccessibleButton className="mx_UserSettings_button"
 | 
			
		||||
                            onClick={this._verifyDevice} data-sigindex={i}>
 | 
			
		||||
                        { _t("Verify...") }
 | 
			
		||||
                    </AccessibleButton></div>;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return <div key={i}>
 | 
			
		||||
                    {sigStatus}
 | 
			
		||||
                    {verifyButton}
 | 
			
		||||
                </div>;
 | 
			
		||||
            });
 | 
			
		||||
            if (this.state.backupSigStatus.sigs.length === 0) {
 | 
			
		||||
                backupSigStatuses = _t("Backup is not signed by any of your devices");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return <div>
 | 
			
		||||
                {_t("Backup version: ")}{this.state.backupInfo.version}<br />
 | 
			
		||||
                {_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
 | 
			
		||||
                {clientBackupStatus}<br />
 | 
			
		||||
                <div>{backupSigStatuses}</div><br />
 | 
			
		||||
                <br />
 | 
			
		||||
                <AccessibleButton className="mx_UserSettings_button"
 | 
			
		||||
                        onClick={this._restoreBackup}>
 | 
			
		||||
                    { _t("Restore backup") }
 | 
			
		||||
                </AccessibleButton>   
 | 
			
		||||
                <AccessibleButton className="mx_UserSettings_button danger"
 | 
			
		||||
                        onClick={this._deleteBackup}>
 | 
			
		||||
                    { _t("Delete backup") }
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
            </div>;
 | 
			
		||||
        } else {
 | 
			
		||||
            return <div>
 | 
			
		||||
                {_t("No backup is present")}<br /><br />
 | 
			
		||||
                <AccessibleButton className="mx_UserSettings_button"
 | 
			
		||||
                        onClick={this._startNewBackup}>
 | 
			
		||||
                    { _t("Start a new backup") }
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -225,6 +225,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",
 | 
			
		||||
@ -309,6 +310,24 @@
 | 
			
		||||
    "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 <b>not</b> uploading keys to this backup": "This device is <b>not</b> uploading keys to this backup",
 | 
			
		||||
    "Backup has a <validity>valid</validity> signature from this device": "Backup has a <validity>valid</validity> signature from this device",
 | 
			
		||||
    "Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device>x</device>": "Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device>x</device>",
 | 
			
		||||
    "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>",
 | 
			
		||||
    "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> device <device></device>": "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> device <device></device>",
 | 
			
		||||
    "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>",
 | 
			
		||||
    "Verify...": "Verify...",
 | 
			
		||||
    "Backup is not signed by any of your devices": "Backup is not signed by any of your devices",
 | 
			
		||||
    "Backup version: ": "Backup version: ",
 | 
			
		||||
    "Algorithm: ": "Algorithm: ",
 | 
			
		||||
    "Restore backup": "Restore backup",
 | 
			
		||||
    "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",
 | 
			
		||||
@ -740,7 +759,6 @@
 | 
			
		||||
    "Unblacklist": "Unblacklist",
 | 
			
		||||
    "Blacklist": "Blacklist",
 | 
			
		||||
    "Unverify": "Unverify",
 | 
			
		||||
    "Verify...": "Verify...",
 | 
			
		||||
    "No results": "No results",
 | 
			
		||||
    "Delete": "Delete",
 | 
			
		||||
    "Communities": "Communities",
 | 
			
		||||
@ -955,6 +973,55 @@
 | 
			
		||||
    "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",
 | 
			
		||||
    "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.",
 | 
			
		||||
    "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.",
 | 
			
		||||
    "Enter a passphrase...": "Enter a passphrase...",
 | 
			
		||||
    "Next": "Next",
 | 
			
		||||
    "If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.": "If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.",
 | 
			
		||||
    "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
 | 
			
		||||
    "That matches!": "That matches!",
 | 
			
		||||
    "That doesn't match.": "That doesn't match.",
 | 
			
		||||
    "Go back to set it again.": "Go back to set it again.",
 | 
			
		||||
    "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.",
 | 
			
		||||
    "Repeat your passphrase...": "Repeat your passphrase...",
 | 
			
		||||
    "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.",
 | 
			
		||||
    "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
 | 
			
		||||
    "Your Recovery Key": "Your Recovery Key",
 | 
			
		||||
    "Copy to clipboard": "Copy to clipboard",
 | 
			
		||||
    "Download": "Download",
 | 
			
		||||
    "I've made a copy": "I've made a copy",
 | 
			
		||||
    "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
 | 
			
		||||
    "Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
 | 
			
		||||
    "<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
 | 
			
		||||
    "<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
 | 
			
		||||
    "<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
 | 
			
		||||
    "Got it": "Got it",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
 | 
			
		||||
    "Set up Secure Message Recovery": "Set up Secure Message Recovery",
 | 
			
		||||
    "Create a Recovery Passphrase": "Create a Recovery Passphrase",
 | 
			
		||||
    "Confirm Recovery Passphrase": "Confirm Recovery Passphrase",
 | 
			
		||||
    "Recovery Key": "Recovery Key",
 | 
			
		||||
    "Keep it safe": "Keep it safe",
 | 
			
		||||
    "Backing up...": "Backing up...",
 | 
			
		||||
    "Create Key Backup": "Create Key Backup",
 | 
			
		||||
    "Unable to create key backup": "Unable to create key backup",
 | 
			
		||||
    "Retry": "Retry",
 | 
			
		||||
    "Unable to load backup status": "Unable to load backup status",
 | 
			
		||||
    "Unable to restore backup": "Unable to restore backup",
 | 
			
		||||
    "No backup found!": "No backup found!",
 | 
			
		||||
    "Backup Restored": "Backup Restored",
 | 
			
		||||
    "Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
 | 
			
		||||
    "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys",
 | 
			
		||||
    "Enter Recovery Passphrase": "Enter Recovery Passphrase",
 | 
			
		||||
    "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
 | 
			
		||||
    "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
 | 
			
		||||
    "Enter Recovery Key": "Enter Recovery Key",
 | 
			
		||||
    "This looks like a valid recovery key!": "This looks like a valid recovery key!",
 | 
			
		||||
    "Not a valid recovery key": "Not a valid recovery key",
 | 
			
		||||
    "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
 | 
			
		||||
    "If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>": "If you've forgotten your recovery passphrase you can <button>set up new recovery options</button>",
 | 
			
		||||
    "Private Chat": "Private Chat",
 | 
			
		||||
    "Public Chat": "Public Chat",
 | 
			
		||||
    "Custom": "Custom",
 | 
			
		||||
@ -1151,6 +1218,7 @@
 | 
			
		||||
    "Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
 | 
			
		||||
    "<not supported>": "<not supported>",
 | 
			
		||||
    "Import E2E room keys": "Import E2E room keys",
 | 
			
		||||
    "Key Backup": "Key Backup",
 | 
			
		||||
    "Cryptography": "Cryptography",
 | 
			
		||||
    "Device ID:": "Device ID:",
 | 
			
		||||
    "Device key:": "Device key:",
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,12 @@ export const SETTINGS = {
 | 
			
		||||
        controller: new LazyLoadingController(),
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
    "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'),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user