/* Copyright 2018, 2019 New Vector Ltd Copyright 2020 The Matrix.org Foundation C.I.C. 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 * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { _t } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../SecurityManager'; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; const RESTORE_TYPE_SECRET_STORAGE = 2; /* * Dialog for restoring e2e keys from a backup and the user's recovery key */ export default class RestoreKeyBackupDialog extends React.PureComponent { static propTypes = { // if false, will close the dialog as soon as the restore completes succesfully // default: true showSummary: PropTypes.bool, // If specified, gather the key from the user but then call the function with the backup // key rather than actually (necessarily) restoring the backup. keyCallback: PropTypes.func, }; static defaultProps = { showSummary: true, }; constructor(props) { super(props); this.state = { backupInfo: null, backupKeyStored: null, loading: false, loadError: null, restoreError: null, recoveryKey: "", recoverInfo: null, recoveryKeyValid: false, forceRecoveryKey: false, passPhrase: '', restoreType: null, progress: { stage: "prefetch" }, }; } componentDidMount() { this._loadBackupStatus(); } _onCancel = () => { this.props.onFinished(false); } _onDone = () => { this.props.onFinished(true); } _onUseRecoveryKeyClick = () => { this.setState({ forceRecoveryKey: true, }); } _progressCallback = (data) => { this.setState({ progress: data, }); } _onResetRecoveryClick = () => { this.props.onFinished(false); accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), }); } _onPassPhraseNext = async () => { this.setState({ loading: true, restoreError: null, restoreType: RESTORE_TYPE_PASSPHRASE, }); try { // We do still restore the key backup: we must ensure that the key backup key // is the right one and restoring it is currently the only way we can do this. const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword( this.state.passPhrase, undefined, undefined, this.state.backupInfo, { progressCallback: this._progressCallback }, ); if (this.props.keyCallback) { const key = await MatrixClientPeg.get().keyBackupKeyFromPassword( this.state.passPhrase, this.state.backupInfo, ); this.props.keyCallback(key); } if (!this.props.showSummary) { this.props.onFinished(true); return; } this.setState({ loading: false, recoverInfo, }); } catch (e) { console.log("Error restoring backup", e); this.setState({ loading: false, restoreError: e, }); } } _onRecoveryKeyNext = async () => { if (!this.state.recoveryKeyValid) return; this.setState({ loading: true, restoreError: null, restoreType: RESTORE_TYPE_RECOVERYKEY, }); try { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey( this.state.recoveryKey, undefined, undefined, this.state.backupInfo, { progressCallback: this._progressCallback }, ); if (this.props.keyCallback) { const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey); this.props.keyCallback(key); } if (!this.props.showSummary) { this.props.onFinished(true); return; } this.setState({ loading: false, recoverInfo, }); } catch (e) { console.log("Error restoring backup", e); this.setState({ loading: false, restoreError: e, }); } } _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); } async _restoreWithSecretStorage() { this.setState({ loading: true, restoreError: null, restoreType: RESTORE_TYPE_SECRET_STORAGE, }); try { // `accessSecretStorage` may prompt for storage access as needed. const recoverInfo = await accessSecretStorage(async () => { return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( this.state.backupInfo, undefined, undefined, { progressCallback: this._progressCallback }, ); }); this.setState({ loading: false, recoverInfo, }); } catch (e) { console.log("Error restoring backup", e); this.setState({ restoreError: e, loading: false, }); } } async _restoreWithCachedKey(backupInfo) { if (!backupInfo) return false; try { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( undefined, /* targetRoomId */ undefined, /* targetSessionId */ backupInfo, { progressCallback: this._progressCallback }, ); this.setState({ recoverInfo, }); return true; } catch (e) { console.log("restoreWithCachedKey failed:", e); return false; } } async _loadBackupStatus() { this.setState({ loading: true, loadError: null, }); try { const cli = MatrixClientPeg.get(); const backupInfo = await cli.getKeyBackupVersion(); const has4S = await cli.hasSecretStorageKey(); const backupKeyStored = has4S && await cli.isKeyBackupKeyStored(); this.setState({ backupInfo, backupKeyStored, }); const gotCache = await this._restoreWithCachedKey(backupInfo); if (gotCache) { console.log("RestoreKeyBackupDialog: found cached backup key"); this.setState({ loading: false, }); return; } // If the backup key is stored, we can proceed directly to restore. if (backupKeyStored) { return this._restoreWithSecretStorage(); } this.setState({ loadError: null, loading: false, }); } catch (e) { console.log("Error loading backup status", e); this.setState({ loadError: e, loading: false, }); } } render() { 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("Restoring keys from backup"); let details; if (this.state.progress.stage === "fetch") { details = _t("Fetching keys from server..."); } else if (this.state.progress.stage === "load_keys") { const { total, successes, failures } = this.state.progress; details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures }); } else if (this.state.progress.stage === "prefetch") { details = _t("Fetching keys from server..."); } content =
{_t( "Backup could not be decrypted with this Security Key: " + "please verify that you entered the correct Security Key.", )}
{_t( "Backup could not be decrypted with this Security Phrase: " + "please verify that you entered the correct Security Phrase.", )}
{_t( "Failed to decrypt %(failedCount)s sessions!", {failedCount: this.state.recoverInfo.total - this.state.recoverInfo.imported}, )}
; } content ={_t("Successfully restored %(sessionCount)s keys", {sessionCount: this.state.recoverInfo.imported})}
{failedToDecrypt}{_t( "Warning: you should only set up key backup " + "from a trusted computer.", {}, { b: sub => {sub} }, )}
{_t( "Access your secure message history and set up secure " + "messaging by entering your Security Phrase.", )}
{_t( "If you've forgotten your Security Phrase you can "+ "{_t( "Warning: You should only set up key backup " + "from a trusted computer.", {}, { b: sub => {sub} }, )}
{_t( "Access your secure message history and set up secure " + "messaging by entering your Security Key.", )}