Move security-related dialogs to a common directory
This commit is contained in:
parent
f4f94e31d1
commit
7bd5e3fa31
28 changed files with 42 additions and 43 deletions
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 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, {createRef} from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t, _td} from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import {copyNode} from "../../../../utils/strings";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
|
||||
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;
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating an e2e key backup
|
||||
* on the server.
|
||||
*/
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._recoveryKeyNode = null;
|
||||
this._keyBackupInfo = null;
|
||||
|
||||
this.state = {
|
||||
secureSecretStorage: null,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
};
|
||||
|
||||
this._passphraseField = createRef();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
this.setState({ secureSecretStorage });
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (secureSecretStorage) {
|
||||
this.setState({ phase: PHASE_BACKINGUP });
|
||||
this._createBackup();
|
||||
}
|
||||
}
|
||||
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onCopyClick = () => {
|
||||
const successful = copyNode(this._recoveryKeyNode);
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDownloadClick = () => {
|
||||
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 = async () => {
|
||||
const { secureSecretStorage } = this.state;
|
||||
this.setState({
|
||||
phase: PHASE_BACKINGUP,
|
||||
error: null,
|
||||
});
|
||||
let info;
|
||||
try {
|
||||
if (secureSecretStorage) {
|
||||
await accessSecretStorage(async () => {
|
||||
info = await MatrixClientPeg.get().prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
|
||||
});
|
||||
} else {
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||
this._keyBackupInfo,
|
||||
);
|
||||
}
|
||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error creating key backup", e);
|
||||
// TODO: If creating a version succeeds, but backup fails, should we
|
||||
// delete the version, disable backup, or do nothing? If we just
|
||||
// disable without deleting, we'll enable on next app reload since
|
||||
// it is trusted.
|
||||
if (info) {
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
|
||||
}
|
||||
this.setState({
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onDone = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onOptOutClick = () => {
|
||||
this.setState({phase: PHASE_OPTOUT_CONFIRM});
|
||||
}
|
||||
|
||||
_onSetUpClick = () => {
|
||||
this.setState({phase: PHASE_PASSPHRASE});
|
||||
}
|
||||
|
||||
_onSkipPassPhraseClick = async () => {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseNextClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!this._passphraseField.current) return; // unmounting
|
||||
|
||||
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this._passphraseField.current.state.valid) {
|
||||
this._passphraseField.current.focus();
|
||||
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmNextClick = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
};
|
||||
|
||||
_onSetAgainClick = () => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
|
||||
_onKeepItSafeBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseValidate = (result) => {
|
||||
this.setState({
|
||||
passPhraseValid: result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"We'll store an encrypted copy of your keys on our server. " +
|
||||
"Secure your backup with a recovery passphrase.",
|
||||
)}</p>
|
||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
||||
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||
onChange={this._onPassPhraseChange}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.passPhrase}
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a recovery passphrase")}
|
||||
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogButtons
|
||||
primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this.state.passPhraseValid}
|
||||
/>
|
||||
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||
{_t("Set up with a recovery key")}
|
||||
</AccessibleButton>
|
||||
</details>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left element-web unattended
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
||||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
"Please enter your recovery passphrase a second time to confirm.",
|
||||
)}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||
<div>
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||
placeholder={_t("Repeat your recovery passphrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
{passPhraseMatch}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseShowKey() {
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Your recovery key is a safety net - you can use it to restore " +
|
||||
"access to your encrypted messages if you forget your recovery passphrase.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
|
||||
)}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
||||
{_t("Your recovery key")}
|
||||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
|
||||
</div>
|
||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
|
||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||
{_t("Copy")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
{_t("Download")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseKeepItSafe() {
|
||||
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("Continue")}
|
||||
onPrimaryButtonClick={this._createBackup}
|
||||
hasCancel={false}>
|
||||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderBusyPhase(text) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseDone() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Your keys are being backed up (the first backup could take a few minutes).",
|
||||
)}</p>
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseOptOutConfirm() {
|
||||
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 session.",
|
||||
)}
|
||||
<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(phase) {
|
||||
switch (phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Secure your backup with a recovery passphrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
return _t('Confirm your recovery passphrase');
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
return _t('Warning!');
|
||||
case PHASE_SHOWKEY:
|
||||
case PHASE_KEEPITSAFE:
|
||||
return _t('Make a copy of your recovery key');
|
||||
case PHASE_BACKINGUP:
|
||||
return _t('Starting backup...');
|
||||
case PHASE_DONE:
|
||||
return _t('Success!');
|
||||
default:
|
||||
return _t("Create key backup");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
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();
|
||||
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_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,825 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 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, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import FileSaver from 'file-saver';
|
||||
import {_t, _td} from '../../../../languageHandler';
|
||||
import Modal from '../../../../Modal';
|
||||
import { promptForBackupPassphrase } from '../../../../SecurityManager';
|
||||
import {copyNode} from "../../../../utils/strings";
|
||||
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton';
|
||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
|
||||
|
||||
const PHASE_LOADING = 0;
|
||||
const PHASE_LOADERROR = 1;
|
||||
const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
|
||||
const PHASE_MIGRATE = 3;
|
||||
const PHASE_PASSPHRASE = 4;
|
||||
const PHASE_PASSPHRASE_CONFIRM = 5;
|
||||
const PHASE_SHOWKEY = 6;
|
||||
const PHASE_STORING = 8;
|
||||
const PHASE_CONFIRM_SKIP = 10;
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
|
||||
// these end up as strings from being values in the radio buttons, so just use strings
|
||||
const CREATE_STORAGE_OPTION_KEY = 'key';
|
||||
const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase';
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a passphrase to guard Secure
|
||||
* Secret Storage in account data.
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
hasCancel: PropTypes.bool,
|
||||
accountPassword: PropTypes.string,
|
||||
forceReset: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
hasCancel: true,
|
||||
forceReset: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._recoveryKey = null;
|
||||
this._recoveryKeyNode = null;
|
||||
this._backupKey = null;
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_LOADING,
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword || "",
|
||||
accountPasswordCorrect: null,
|
||||
passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
|
||||
canSkip: !isSecureBackupRequired(),
|
||||
};
|
||||
|
||||
this._passphraseField = createRef();
|
||||
|
||||
this._fetchBackupInfo();
|
||||
if (this.state.accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
// signing key upload as well. This avoids hitting the server to
|
||||
// test auth flows, which may be slow under high load.
|
||||
this.state.canUploadKeysWithPasswordOnly = true;
|
||||
} else {
|
||||
this._queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
async _fetchBackupInfo() {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = (
|
||||
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
||||
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
|
||||
);
|
||||
|
||||
const { forceReset } = this.props;
|
||||
const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
|
||||
|
||||
this.setState({
|
||||
phase,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
};
|
||||
} catch (e) {
|
||||
this.setState({phase: PHASE_LOADERROR});
|
||||
}
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
try {
|
||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
} catch (error) {
|
||||
if (!error.data || !error.data.flows) {
|
||||
console.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
return;
|
||||
}
|
||||
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
|
||||
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
|
||||
});
|
||||
this.setState({
|
||||
canUploadKeysWithPasswordOnly,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyBackupStatusChange = () => {
|
||||
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
_onKeyPassphraseChange = e => {
|
||||
this.setState({
|
||||
passPhraseKeySelected: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onChooseKeyPassphraseFormSubmit = async () => {
|
||||
if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
|
||||
this._recoveryKey =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onMigrateFormSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.state.backupSigStatus.usable) {
|
||||
this._bootstrapSecretStorage();
|
||||
} else {
|
||||
this._restoreBackup();
|
||||
}
|
||||
}
|
||||
|
||||
_onCopyClick = () => {
|
||||
const successful = copyNode(this._recoveryKeyNode);
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDownloadClick = () => {
|
||||
const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'recovery-key.txt');
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
});
|
||||
}
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
},
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
body: _t("To continue, use Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("Confirm encryption setup"),
|
||||
body: _t("Click the button below to confirm setting up encryption."),
|
||||
continueText: _t("Confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bootstrapSecretStorage = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_STORING,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const { forceReset } = this.props;
|
||||
|
||||
try {
|
||||
// JRS: In an upcoming change, the cross-signing steps will be
|
||||
// removed from here and this will instead be about secret storage
|
||||
// only.
|
||||
if (forceReset) {
|
||||
console.log("Forcing cross-signing and secret storage reset");
|
||||
await cli.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this._recoveryKey,
|
||||
setupNewKeyBackup: true,
|
||||
setupNewSecretStorage: true,
|
||||
});
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
} else {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
});
|
||||
await cli.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this._recoveryKey,
|
||||
keyBackupInfo: this.state.backupInfo,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
getKeyBackupPassphrase: () => {
|
||||
// We may already have the backup key if we earlier went
|
||||
// through the restore backup path, so pass it along
|
||||
// rather than prompting again.
|
||||
if (this._backupKey) {
|
||||
return this._backupKey;
|
||||
}
|
||||
return promptForBackupPassphrase();
|
||||
},
|
||||
});
|
||||
}
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
|
||||
this.setState({
|
||||
accountPassword: '',
|
||||
accountPasswordCorrect: false,
|
||||
phase: PHASE_MIGRATE,
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: e });
|
||||
}
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onDone = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_restoreBackup = async () => {
|
||||
// It's possible we'll need the backup key later on for bootstrapping,
|
||||
// so let's stash it here, rather than prompting for it twice.
|
||||
const keyCallback = k => this._backupKey = k;
|
||||
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog,
|
||||
{
|
||||
showSummary: false,
|
||||
keyCallback,
|
||||
},
|
||||
null, /* priority = */ false, /* static = */ false,
|
||||
);
|
||||
|
||||
await finished;
|
||||
const { backupSigStatus } = await this._fetchBackupInfo();
|
||||
if (
|
||||
backupSigStatus.usable &&
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
this.state.accountPassword
|
||||
) {
|
||||
this._bootstrapSecretStorage();
|
||||
}
|
||||
}
|
||||
|
||||
_onLoadRetryClick = () => {
|
||||
this.setState({phase: PHASE_LOADING});
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
_onShowKeyContinueClick = () => {
|
||||
this._bootstrapSecretStorage();
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.setState({phase: PHASE_CONFIRM_SKIP});
|
||||
}
|
||||
|
||||
_onGoBackClick = () => {
|
||||
this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE});
|
||||
}
|
||||
|
||||
_onPassPhraseNextClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!this._passphraseField.current) return; // unmounting
|
||||
|
||||
await this._passphraseField.current.validate({ allowEmpty: false });
|
||||
if (!this._passphraseField.current.state.valid) {
|
||||
this._passphraseField.current.focus();
|
||||
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||
};
|
||||
|
||||
_onPassPhraseConfirmNextClick = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
||||
this._recoveryKey =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
setPassphrase: true,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onSetAgainClick = () => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseValid: false,
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseValidate = (result) => {
|
||||
this.setState({
|
||||
passPhraseValid: result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_onAccountPasswordChange = (e) => {
|
||||
this.setState({
|
||||
accountPassword: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_renderPhaseChooseKeyPassphrase() {
|
||||
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
|
||||
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
|
||||
"Safeguard against losing access to encrypted messages & data by " +
|
||||
"backing up encryption keys on your server.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}>
|
||||
<StyledRadioButton
|
||||
key={CREATE_STORAGE_OPTION_KEY}
|
||||
value={CREATE_STORAGE_OPTION_KEY}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
|
||||
{_t("Generate a Security Key")}
|
||||
</div>
|
||||
<div>{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
key={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
|
||||
{_t("Enter a Security Phrase")}
|
||||
</div>
|
||||
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit}
|
||||
onCancel={this._onCancelClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseMigrate() {
|
||||
// TODO: This is a temporary screen so people who have the labs flag turned on and
|
||||
// click the button are aware they're making a change to their account.
|
||||
// Once we're confident enough in this (and it's supported enough) we can do
|
||||
// it automatically.
|
||||
// https://github.com/vector-im/element-web/issues/11696
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let authPrompt;
|
||||
let nextCaption = _t("Next");
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||
<div><Field
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
value={this.state.accountPassword}
|
||||
onChange={this._onAccountPasswordChange}
|
||||
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
|
||||
autoFocus={true}
|
||||
/></div>
|
||||
</div>;
|
||||
} else if (!this.state.backupSigStatus.usable) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||
</div>;
|
||||
nextCaption = _t("Restore");
|
||||
} else {
|
||||
authPrompt = <p>
|
||||
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
|
||||
</p>;
|
||||
}
|
||||
|
||||
return <form onSubmit={this._onMigrateFormSubmit}>
|
||||
<p>{_t(
|
||||
"Upgrade this session to allow it to verify other sessions, " +
|
||||
"granting them access to encrypted messages and marking them " +
|
||||
"as trusted for other users.",
|
||||
)}</p>
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this._onCancelClick}>
|
||||
{_t('Skip')}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||
<p>{_t(
|
||||
"Enter a security phrase only you know, as it’s used to safeguard your data. " +
|
||||
"To be secure, you shouldn’t re-use your account password.",
|
||||
)}</p>
|
||||
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<PassphraseField
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
onChange={this._onPassPhraseChange}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.passPhrase}
|
||||
onValidate={this._onPassPhraseValidate}
|
||||
fieldRef={this._passphraseField}
|
||||
autoFocus={true}
|
||||
label={_td("Enter a recovery passphrase")}
|
||||
labelEnterPassword={_td("Enter a recovery passphrase")}
|
||||
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this.state.passPhraseValid}
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
className="danger"
|
||||
>{_t("Cancel")}</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left element-web unattended
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div>
|
||||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||
<p>{_t(
|
||||
"Enter your recovery passphrase a second time to confirm it.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field
|
||||
type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
label={_t("Confirm your recovery passphrase")}
|
||||
autoFocus={true}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||
{passPhraseMatch}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancelClick}
|
||||
className="danger"
|
||||
>{_t("Skip")}</button>
|
||||
</DialogButtons>
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhaseShowKey() {
|
||||
let continueButton;
|
||||
if (this.state.phase === PHASE_SHOWKEY) {
|
||||
continueButton = <DialogButtons primaryButton={_t("Continue")}
|
||||
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
|
||||
onPrimaryButtonClick={this._onShowKeyContinueClick}
|
||||
hasCancel={false}
|
||||
/>;
|
||||
} else {
|
||||
continueButton = <div className="mx_CreateSecretStorageDialog_continueSpinner">
|
||||
<InlineSpinner />
|
||||
</div>;
|
||||
}
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Store your Security Key somewhere safe, like a password manager or a safe, " +
|
||||
"as it’s used to safeguard your encrypted data.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||
<AccessibleButton kind='primary' className="mx_Dialog_primary"
|
||||
onClick={this._onDownloadClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
>
|
||||
{_t("Download")}
|
||||
</AccessibleButton>
|
||||
<span>{_t("or")}</span>
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
|
||||
onClick={this._onCopyClick}
|
||||
disabled={this.state.phase === PHASE_STORING}
|
||||
>
|
||||
{this.state.copied ? _t("Copied!") : _t("Copy")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{continueButton}
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderBusyPhase() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseLoadError() {
|
||||
return <div>
|
||||
<p>{_t("Unable to query secret storage status")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._onLoadRetryClick}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseSkipConfirm() {
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"You can also set up Secure Backup & manage your keys in Settings.",
|
||||
)}</p>
|
||||
<DialogButtons primaryButton={_t('Go back')}
|
||||
onPrimaryButtonClick={this._onGoBackClick}
|
||||
hasCancel={false}
|
||||
>
|
||||
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_titleForPhase(phase) {
|
||||
switch (phase) {
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
return _t('Set up Secure Backup');
|
||||
case PHASE_MIGRATE:
|
||||
return _t('Upgrade your encryption');
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Set a Security Phrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
return _t('Confirm Security Phrase');
|
||||
case PHASE_CONFIRM_SKIP:
|
||||
return _t('Are you sure?');
|
||||
case PHASE_SHOWKEY:
|
||||
return _t('Save your Security Key');
|
||||
case PHASE_STORING:
|
||||
return _t('Setting up keys');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = <div>
|
||||
<p>{_t("Unable to set up secret storage")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
hasCancel={this.state.canSkip}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case PHASE_LOADING:
|
||||
content = this._renderBusyPhase();
|
||||
break;
|
||||
case PHASE_LOADERROR:
|
||||
content = this._renderPhaseLoadError();
|
||||
break;
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
content = this._renderPhaseChooseKeyPassphrase();
|
||||
break;
|
||||
case PHASE_MIGRATE:
|
||||
content = this._renderPhaseMigrate();
|
||||
break;
|
||||
case PHASE_PASSPHRASE:
|
||||
content = this._renderPhasePassPhrase();
|
||||
break;
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
content = this._renderPhasePassPhraseConfirm();
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
content = this._renderPhaseShowKey();
|
||||
break;
|
||||
case PHASE_STORING:
|
||||
content = this._renderBusyPhase();
|
||||
break;
|
||||
case PHASE_CONFIRM_SKIP:
|
||||
content = this._renderPhaseSkipConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let titleClass = null;
|
||||
switch (this.state.phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
titleClass = [
|
||||
'mx_CreateSecretStorageDialog_titleWithIcon',
|
||||
'mx_CreateSecretStorageDialog_securePhraseTitle',
|
||||
];
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
titleClass = [
|
||||
'mx_CreateSecretStorageDialog_titleWithIcon',
|
||||
'mx_CreateSecretStorageDialog_secureBackupTitle',
|
||||
];
|
||||
break;
|
||||
case PHASE_CHOOSE_KEY_PASSPHRASE:
|
||||
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_CreateSecretStorageDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={this._titleForPhase(this.state.phase)}
|
||||
titleClass={titleClass}
|
||||
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
|
||||
const PHASE_EDIT = 1;
|
||||
const PHASE_EXPORTING = 2;
|
||||
|
||||
export default class ExportE2eKeysDialog extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._passphrase1 = createRef();
|
||||
this._passphrase2 = createRef();
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_EDIT,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onPassphraseFormSubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const passphrase = this._passphrase1.current.value;
|
||||
if (passphrase !== this._passphrase2.current.value) {
|
||||
this.setState({errStr: _t('Passphrases must match')});
|
||||
return false;
|
||||
}
|
||||
if (!passphrase) {
|
||||
this.setState({errStr: _t('Passphrase must not be empty')});
|
||||
return false;
|
||||
}
|
||||
|
||||
this._startExport(passphrase);
|
||||
return false;
|
||||
};
|
||||
|
||||
_startExport(passphrase) {
|
||||
// extra Promise.resolve() to turn synchronous exceptions into
|
||||
// asynchronous ones.
|
||||
Promise.resolve().then(() => {
|
||||
return this.props.matrixClient.exportRoomKeys();
|
||||
}).then((k) => {
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||
JSON.stringify(k), passphrase,
|
||||
);
|
||||
}).then((f) => {
|
||||
const blob = new Blob([f], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'element-keys.txt');
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
console.error("Error exporting e2e keys:", e);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t('Unknown error');
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: PHASE_EDIT,
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: PHASE_EXPORTING,
|
||||
});
|
||||
}
|
||||
|
||||
_onCancelClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Export room keys")}
|
||||
>
|
||||
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t(
|
||||
'This process allows you to export the keys for messages ' +
|
||||
'you have received in encrypted rooms to a local file. You ' +
|
||||
'will then be able to import the file into another Matrix ' +
|
||||
'client in the future, so that client will also be able to ' +
|
||||
'decrypt these messages.',
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
'The exported file will allow anyone who can read it to decrypt ' +
|
||||
'any encrypted messages that you can see, so you should be ' +
|
||||
'careful to keep it secure. To help with this, you should enter ' +
|
||||
'a passphrase below, which will be used to encrypt the exported ' +
|
||||
'data. It will only be possible to import the data by using the ' +
|
||||
'same passphrase.',
|
||||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase1'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this._passphrase1} id='passphrase1'
|
||||
autoFocus={true} size='64' type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase2'>
|
||||
{ _t("Confirm passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this._passphrase2} id='passphrase2'
|
||||
size='64' type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 * as sdk from "../../../../index";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
export default class IgnoreRecoveryReminderDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onDontAskAgain: PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
onSetup: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onDontAskAgainClick = () => {
|
||||
this.props.onFinished();
|
||||
this.props.onDontAskAgain();
|
||||
}
|
||||
|
||||
onSetupClick = () => {
|
||||
this.props.onFinished();
|
||||
this.props.onSetup();
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_IgnoreRecoveryReminderDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Are you sure?")}
|
||||
>
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Without setting up Secure Message Recovery, " +
|
||||
"you'll lose your secure message history when you " +
|
||||
"log out.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"If you don't want to set this up now, you can later " +
|
||||
"in Settings.",
|
||||
)}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons
|
||||
primaryButton={_t("Set up")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
cancelButton={_t("Don't ask again")}
|
||||
onCancel={this.onDontAskAgainClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
function readFileAsArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
const PHASE_EDIT = 1;
|
||||
const PHASE_IMPORTING = 2;
|
||||
|
||||
export default class ImportE2eKeysDialog extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._file = createRef();
|
||||
this._passphrase = createRef();
|
||||
|
||||
this.state = {
|
||||
enableSubmit: false,
|
||||
phase: PHASE_EDIT,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onFormChange = (ev) => {
|
||||
const files = this._file.current.files || [];
|
||||
this.setState({
|
||||
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
|
||||
});
|
||||
};
|
||||
|
||||
_onFormSubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
this._startImport(this._file.current.files[0], this._passphrase.current.value);
|
||||
return false;
|
||||
};
|
||||
|
||||
_startImport(file, passphrase) {
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: PHASE_IMPORTING,
|
||||
});
|
||||
|
||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||
arrayBuffer, passphrase,
|
||||
);
|
||||
}).then((keys) => {
|
||||
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
||||
}).then(() => {
|
||||
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
console.error("Error importing e2e keys:", e);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
const msg = e.friendlyText || _t('Unknown error');
|
||||
this.setState({
|
||||
errStr: msg,
|
||||
phase: PHASE_EDIT,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onCancelClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_importE2eKeysDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Import room keys")}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t(
|
||||
'This process allows you to import encryption keys ' +
|
||||
'that you had previously exported from another Matrix ' +
|
||||
'client. You will then be able to decrypt any ' +
|
||||
'messages that the other client could decrypt.',
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
'The export file will be protected with a passphrase. ' +
|
||||
'You should enter the passphrase here, to decrypt the file.',
|
||||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='importFile'>
|
||||
{ _t("File to import") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._file}
|
||||
id='importFile'
|
||||
type='file'
|
||||
autoFocus={true}
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this._passphrase}
|
||||
id='passphrase'
|
||||
size='64'
|
||||
type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_Dialog_buttons'>
|
||||
<input className='mx_Dialog_primary' type='submit' value={_t('Import')}
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
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 dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import {Action} from "../../../../dispatcher/actions";
|
||||
|
||||
export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// As returned by js-sdk getKeyBackupVersion()
|
||||
newVersionInfo: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onOkClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onGoToSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
onSetupClick = async () => {
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
onFinished: this.props.onFinished,
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{_t("New Recovery Method")}
|
||||
</span>;
|
||||
|
||||
const newMethodDetected = <p>{_t(
|
||||
"A new recovery passphrase and key for Secure Messages have been detected.",
|
||||
)}</p>;
|
||||
|
||||
const hackWarning = <p className="warning">{_t(
|
||||
"If you didn't set the new recovery method, an " +
|
||||
"attacker may be trying to access your account. " +
|
||||
"Change your account password and set a new recovery " +
|
||||
"method immediately in Settings.",
|
||||
)}</p>;
|
||||
|
||||
let content;
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
content = <div>
|
||||
{newMethodDetected}
|
||||
<p>{_t(
|
||||
"This session is encrypting history using the new recovery method.",
|
||||
)}</p>
|
||||
{hackWarning}
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
onPrimaryButtonClick={this.onOkClick}
|
||||
cancelButton={_t("Go to Settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>;
|
||||
} else {
|
||||
content = <div>
|
||||
{newMethodDetected}
|
||||
{hackWarning}
|
||||
<DialogButtons
|
||||
primaryButton={_t("Set up Secure Messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
cancelButton={_t("Go to Settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_KeyBackupFailedDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright 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 dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import {Action} from "../../../../dispatcher/actions";
|
||||
|
||||
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onGoToSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
onSetupClick = () => {
|
||||
this.props.onFinished();
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("./CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||
|
||||
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||
{_t("Recovery Method Removed")}
|
||||
</span>;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_KeyBackupFailedDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
<p>{_t(
|
||||
"This session has detected that your recovery passphrase and key " +
|
||||
"for Secure Messages have been removed.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"If you did this accidentally, you can setup Secure Messages on " +
|
||||
"this session which will re-encrypt this session's message " +
|
||||
"history with a new recovery method.",
|
||||
)}</p>
|
||||
<p className="warning">{_t(
|
||||
"If you didn't remove the recovery method, an " +
|
||||
"attacker may be trying to access your account. " +
|
||||
"Change your account password and set a new recovery " +
|
||||
"method immediately in Settings.",
|
||||
)}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Set up Secure Messages")}
|
||||
onPrimaryButtonClick={this.onSetupClick}
|
||||
cancelButton={_t("Go to Settings")}
|
||||
onCancel={this.onGoToSettingsClick}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue