Convert /src/async-components/views/dialogs/security to TS (#6923)

* Convert RecoveryMethodRemovedDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Convert NewRecoveryMethodDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Convert ImportE2eKeysDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Convert ExportE2eKeysDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Convert CreateSecretStorageDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Convert CreateKeyBackupDialog to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix types

This is somewhat hacky though I don't know of a better way to do this

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-10-23 05:32:16 +02:00 committed by GitHub
parent 39e98b9d7f
commit c5bd1fb32d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 535 additions and 505 deletions

View file

@ -32,6 +32,7 @@ import SecurityCustomisations from "./customisations/Security";
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ComponentType } from "react";
// This stores the secret storage private keys in memory for the JS SDK. This is // This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times // only meant to act as a cache to avoid prompting the user multiple times
@ -335,7 +336,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
// This dialog calls bootstrap itself after guiding the user through // This dialog calls bootstrap itself after guiding the user through
// passphrase creation. // passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
import("./async-components/views/dialogs/security/CreateSecretStorageDialog"), import(
"./async-components/views/dialogs/security/CreateSecretStorageDialog"
) as unknown as Promise<ComponentType<{}>>,
{ {
forceReset, forceReset,
}, },

View file

@ -17,56 +17,70 @@ limitations under the License.
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../../languageHandler'; import { _t, _td } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager'; import { accessSecretStorage } from '../../../../SecurityManager';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import { copyNode } from "../../../../utils/strings"; import { copyNode } from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField"; import PassphraseField from "../../../../components/views/auth/PassphraseField";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field";
import Spinner from "../../../../components/views/elements/Spinner";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IValidationResult } from "../../../../components/views/elements/Validation";
import { IPreparedKeyBackupVersion } from "matrix-js-sdk/src/crypto/backup";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
const PHASE_PASSPHRASE = 0; enum Phase {
const PHASE_PASSPHRASE_CONFIRM = 1; Passphrase = "passphrase",
const PHASE_SHOWKEY = 2; PassphraseConfirm = "passphrase_confirm",
const PHASE_KEEPITSAFE = 3; ShowKey = "show_key",
const PHASE_BACKINGUP = 4; KeepItSafe = "keep_it_safe",
const PHASE_DONE = 5; BackingUp = "backing_up",
const PHASE_OPTOUT_CONFIRM = 6; Done = "done",
OptOutConfirm = "opt_out_confirm",
}
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
interface IProps extends IDialogProps {}
interface IState {
secureSecretStorage: boolean;
phase: Phase;
passPhrase: string;
passPhraseValid: boolean;
passPhraseConfirm: string;
copied: boolean;
downloaded: boolean;
error?: string;
}
/* /*
* Walks the user through the process of creating an e2e key backup * Walks the user through the process of creating an e2e key backup
* on the server. * on the server.
*/ */
export default class CreateKeyBackupDialog extends React.PureComponent { export default class CreateKeyBackupDialog extends React.PureComponent<IProps, IState> {
static propTypes = { private keyBackupInfo: Pick<IPreparedKeyBackupVersion, "recovery_key" | "algorithm" | "auth_data">;
onFinished: PropTypes.func.isRequired, private recoveryKeyNode = createRef<HTMLElement>();
} private passphraseField = createRef<Field>();
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._recoveryKeyNode = null;
this._keyBackupInfo = null;
this.state = { this.state = {
secureSecretStorage: null, secureSecretStorage: null,
phase: PHASE_PASSPHRASE, phase: Phase.Passphrase,
passPhrase: '', passPhrase: '',
passPhraseValid: false, passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
copied: false, copied: false,
downloaded: false, downloaded: false,
}; };
this._passphraseField = createRef();
} }
async componentDidMount() { public async componentDidMount(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
this.setState({ secureSecretStorage }); this.setState({ secureSecretStorage });
@ -74,41 +88,37 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
// If we're using secret storage, skip ahead to the backing up step, as // If we're using secret storage, skip ahead to the backing up step, as
// `accessSecretStorage` will handle passphrases as needed. // `accessSecretStorage` will handle passphrases as needed.
if (secureSecretStorage) { if (secureSecretStorage) {
this.setState({ phase: PHASE_BACKINGUP }); this.setState({ phase: Phase.BackingUp });
this._createBackup(); this.createBackup();
} }
} }
_collectRecoveryKeyNode = (n) => { private onCopyClick = (): void => {
this._recoveryKeyNode = n; const successful = copyNode(this.recoveryKeyNode.current);
}
_onCopyClick = () => {
const successful = copyNode(this._recoveryKeyNode);
if (successful) { if (successful) {
this.setState({ this.setState({
copied: true, copied: true,
phase: PHASE_KEEPITSAFE, phase: Phase.KeepItSafe,
}); });
} }
} };
_onDownloadClick = () => { private onDownloadClick = (): void => {
const blob = new Blob([this._keyBackupInfo.recovery_key], { const blob = new Blob([this.keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'security-key.txt'); FileSaver.saveAs(blob, 'security-key.txt');
this.setState({ this.setState({
downloaded: true, downloaded: true,
phase: PHASE_KEEPITSAFE, phase: Phase.KeepItSafe,
}); });
} };
_createBackup = async () => { private createBackup = async (): Promise<void> => {
const { secureSecretStorage } = this.state; const { secureSecretStorage } = this.state;
this.setState({ this.setState({
phase: PHASE_BACKINGUP, phase: Phase.BackingUp,
error: null, error: null,
}); });
let info; let info;
@ -123,12 +133,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
}); });
} else { } else {
info = await MatrixClientPeg.get().createKeyBackupVersion( info = await MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo, this.keyBackupInfo,
); );
} }
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
this.setState({ this.setState({
phase: PHASE_DONE, phase: Phase.Done,
}); });
} catch (e) { } catch (e) {
logger.error("Error creating key backup", e); logger.error("Error creating key backup", e);
@ -143,97 +153,91 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
error: e, error: e,
}); });
} }
} };
_onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onDone = () => { private onDone = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
} };
_onOptOutClick = () => { private onSetUpClick = (): void => {
this.setState({ phase: PHASE_OPTOUT_CONFIRM }); this.setState({ phase: Phase.Passphrase });
} };
_onSetUpClick = () => { private onSkipPassPhraseClick = async (): Promise<void> => {
this.setState({ phase: PHASE_PASSPHRASE }); this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
}
_onSkipPassPhraseClick = async () => {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
phase: PHASE_SHOWKEY, phase: Phase.ShowKey,
}); });
} };
_onPassPhraseNextClick = async (e) => { private onPassPhraseNextClick = async (e: React.FormEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();
if (!this._passphraseField.current) return; // unmounting if (!this.passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false }); await this.passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) { if (!this.passphraseField.current.state.valid) {
this._passphraseField.current.focus(); this.passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true }); this.passphraseField.current.validate({ allowEmpty: false, focused: true });
return; return;
} }
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM }); this.setState({ phase: Phase.PassphraseConfirm });
}; };
_onPassPhraseConfirmNextClick = async (e) => { private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return; if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
phase: PHASE_SHOWKEY, phase: Phase.ShowKey,
}); });
}; };
_onSetAgainClick = () => { private onSetAgainClick = (): void => {
this.setState({ this.setState({
passPhrase: '', passPhrase: '',
passPhraseValid: false, passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
phase: PHASE_PASSPHRASE, phase: Phase.Passphrase,
}); });
} };
_onKeepItSafeBackClick = () => { private onKeepItSafeBackClick = (): void => {
this.setState({ this.setState({
phase: PHASE_SHOWKEY, phase: Phase.ShowKey,
}); });
} };
_onPassPhraseValidate = (result) => { private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({ this.setState({
passPhraseValid: result.valid, passPhraseValid: result.valid,
}); });
}; };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
} };
_onPassPhraseConfirmChange = (e) => { private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
passPhraseConfirm: e.target.value, passPhraseConfirm: e.target.value,
}); });
} };
_renderPhasePassPhrase() { private renderPhasePassPhrase(): JSX.Element {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return <form onSubmit={this.onPassPhraseNextClick}>
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{ _t( <p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {}, "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{ sub }</b> }, { b: sub => <b>{ sub }</b> },
@ -248,11 +252,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<PassphraseField <PassphraseField
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE} minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase} value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate} onValidate={this.onPassPhraseValidate}
fieldRef={this._passphraseField} fieldRef={this.passphraseField}
autoFocus={true} autoFocus={true}
label={_td("Enter a Security Phrase")} label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")} labelEnterPassword={_td("Enter a Security Phrase")}
@ -264,23 +268,21 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<DialogButtons <DialogButtons
primaryButton={_t('Next')} primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick} onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false} hasCancel={false}
disabled={!this.state.passPhraseValid} disabled={!this.state.passPhraseValid}
/> />
<details> <details>
<summary>{ _t("Advanced") }</summary> <summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}> <AccessibleButton kind='primary' onClick={this.onSkipPassPhraseClick}>
{ _t("Set up with a Security Key") } { _t("Set up with a Security Key") }
</AccessibleButton> </AccessibleButton>
</details> </details>
</form>; </form>;
} }
_renderPhasePassPhraseConfirm() { private renderPhasePassPhraseConfirm(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText; let matchText;
let changeText; let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) { if (this.state.passPhraseConfirm === this.state.passPhrase) {
@ -303,14 +305,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch"> passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{ matchText }</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
{ changeText } { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return <form onSubmit={this.onPassPhraseConfirmNextClick}>
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{ _t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
) }</p> ) }</p>
@ -318,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div> <div>
<input type="password" <input type="password"
onChange={this._onPassPhraseConfirmChange} onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your Security Phrase...")} placeholder={_t("Repeat your Security Phrase...")}
@ -330,14 +331,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t('Next')} primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick} onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false} hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm} disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/> />
</form>; </form>;
} }
_renderPhaseShowKey() { private renderPhaseShowKey(): JSX.Element {
return <div> return <div>
<p>{ _t( <p>{ _t(
"Your Security Key is a safety net - you can use it to restore " + "Your Security Key is a safety net - you can use it to restore " +
@ -352,13 +353,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer"> <div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey"> <div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code> <code ref={this.recoveryKeyNode}>{ this.keyBackupInfo.recovery_key }</code>
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons"> <div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}> <button className="mx_Dialog_primary" onClick={this.onCopyClick}>
{ _t("Copy") } { _t("Copy") }
</button> </button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}> <button className="mx_Dialog_primary" onClick={this.onDownloadClick}>
{ _t("Download") } { _t("Download") }
</button> </button>
</div> </div>
@ -367,7 +368,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
</div>; </div>;
} }
_renderPhaseKeepItSafe() { private renderPhaseKeepItSafe(): JSX.Element {
let introText; let introText;
if (this.state.copied) { if (this.state.copied) {
introText = _t( introText = _t(
@ -380,7 +381,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{}, { b: s => <b>{ s }</b> }, {}, { b: s => <b>{ s }</b> },
); );
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{ introText } { introText }
<ul> <ul>
@ -389,107 +389,101 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li> <li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul> </ul>
<DialogButtons primaryButton={_t("Continue")} <DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this.createBackup}
hasCancel={false}> hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button> <button onClick={this.onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
_renderBusyPhase(text) { private renderBusyPhase(): JSX.Element {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div> return <div>
<Spinner /> <Spinner />
</div>; </div>;
} }
_renderPhaseDone() { private renderPhaseDone(): JSX.Element {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{ _t( <p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).", "Your keys are being backed up (the first backup could take a few minutes).",
) }</p> ) }</p>
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this.onDone}
hasCancel={false} hasCancel={false}
/> />
</div>; </div>;
} }
_renderPhaseOptOutConfirm() { private renderPhaseOptOutConfirm(): JSX.Element {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{ _t( { _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " + "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.", "encrypted message history if you log out or use another session.",
) } ) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')} <DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick} onPrimaryButtonClick={this.onSetUpClick}
hasCancel={false} hasCancel={false}
> >
<button onClick={this._onCancel}>I understand, continue without</button> <button onClick={this.onCancel}>I understand, continue without</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
_titleForPhase(phase) { private titleForPhase(phase: Phase): string {
switch (phase) { switch (phase) {
case PHASE_PASSPHRASE: case Phase.Passphrase:
return _t('Secure your backup with a Security Phrase'); return _t('Secure your backup with a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM: case Phase.PassphraseConfirm:
return _t('Confirm your Security Phrase'); return _t('Confirm your Security Phrase');
case PHASE_OPTOUT_CONFIRM: case Phase.OptOutConfirm:
return _t('Warning!'); return _t('Warning!');
case PHASE_SHOWKEY: case Phase.ShowKey:
case PHASE_KEEPITSAFE: case Phase.KeepItSafe:
return _t('Make a copy of your Security Key'); return _t('Make a copy of your Security Key');
case PHASE_BACKINGUP: case Phase.BackingUp:
return _t('Starting backup...'); return _t('Starting backup...');
case PHASE_DONE: case Phase.Done:
return _t('Success!'); return _t('Success!');
default: default:
return _t("Create key backup"); return _t("Create key backup");
} }
} }
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content; let content;
if (this.state.error) { if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div> content = <div>
<p>{ _t("Unable to create key backup") }</p> <p>{ _t("Unable to create key backup") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this.createBackup}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
/> />
</div> </div>
</div>; </div>;
} else { } else {
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_PASSPHRASE: case Phase.Passphrase:
content = this._renderPhasePassPhrase(); content = this.renderPhasePassPhrase();
break; break;
case PHASE_PASSPHRASE_CONFIRM: case Phase.PassphraseConfirm:
content = this._renderPhasePassPhraseConfirm(); content = this.renderPhasePassPhraseConfirm();
break; break;
case PHASE_SHOWKEY: case Phase.ShowKey:
content = this._renderPhaseShowKey(); content = this.renderPhaseShowKey();
break; break;
case PHASE_KEEPITSAFE: case Phase.KeepItSafe:
content = this._renderPhaseKeepItSafe(); content = this.renderPhaseKeepItSafe();
break; break;
case PHASE_BACKINGUP: case Phase.BackingUp:
content = this._renderBusyPhase(); content = this.renderBusyPhase();
break; break;
case PHASE_DONE: case Phase.Done:
content = this._renderPhaseDone(); content = this.renderPhaseDone();
break; break;
case PHASE_OPTOUT_CONFIRM: case Phase.OptOutConfirm:
content = this._renderPhaseOptOutConfirm(); content = this.renderPhaseOptOutConfirm();
break; break;
} }
} }
@ -497,8 +491,8 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
return ( return (
<BaseDialog className='mx_CreateKeyBackupDialog' <BaseDialog className='mx_CreateKeyBackupDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)} title={this.titleForPhase(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} hasCancel={[Phase.Passphrase, Phase.Done].includes(this.state.phase)}
> >
<div> <div>
{ content } { content }

View file

@ -16,8 +16,6 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { _t, _td } from '../../../../languageHandler'; import { _t, _td } from '../../../../languageHandler';
@ -31,52 +29,105 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; import {
getSecureBackupSetupMethods,
isSecureBackupRequired,
SecureBackupSetupMethod,
} from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security"; import SecurityCustomisations from "../../../../customisations/Security";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { CrossSigningKeys } from "matrix-js-sdk";
import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog";
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
import { IValidationResult } from "../../../../components/views/elements/Validation";
const PHASE_LOADING = 0; // I made a mistake while converting this and it has to be fixed!
const PHASE_LOADERROR = 1; enum Phase {
const PHASE_CHOOSE_KEY_PASSPHRASE = 2; Loading = "loading",
const PHASE_MIGRATE = 3; LoadError = "load_error",
const PHASE_PASSPHRASE = 4; ChooseKeyPassphrase = "choose_key_passphrase",
const PHASE_PASSPHRASE_CONFIRM = 5; Migrate = "migrate",
const PHASE_SHOWKEY = 6; Passphrase = "passphrase",
const PHASE_STORING = 8; PassphraseConfirm = "passphrase_confirm",
const PHASE_CONFIRM_SKIP = 10; ShowKey = "show_key",
Storing = "storing",
ConfirmSkip = "confirm_skip",
}
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. 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 interface IProps extends IDialogProps {
const CREATE_STORAGE_OPTION_KEY = 'key'; hasCancel: boolean;
const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase'; accountPassword: string;
forceReset: boolean;
}
interface IState {
phase: Phase;
passPhrase: string;
passPhraseValid: boolean;
passPhraseConfirm: string;
copied: boolean;
downloaded: boolean;
setPassphrase: boolean;
backupInfo: IKeyBackupInfo;
backupSigStatus: TrustInfo;
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: boolean;
accountPassword: string;
accountPasswordCorrect: boolean;
canSkip: boolean;
passPhraseKeySelected: string;
error?: string;
}
/* /*
* Walks the user through the process of creating a passphrase to guard Secure * Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data. * Secret Storage in account data.
*/ */
export default class CreateSecretStorageDialog extends React.PureComponent { export default class CreateSecretStorageDialog extends React.PureComponent<IProps, IState> {
static propTypes = { public static defaultProps: Partial<IProps> = {
hasCancel: PropTypes.bool,
accountPassword: PropTypes.string,
forceReset: PropTypes.bool,
};
static defaultProps = {
hasCancel: true, hasCancel: true,
forceReset: false, forceReset: false,
}; };
private recoveryKey: IRecoveryKey;
private backupKey: Uint8Array;
private recoveryKeyNode = createRef<HTMLElement>();
private passphraseField = createRef<Field>();
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._recoveryKey = null; let passPhraseKeySelected;
this._recoveryKeyNode = null; const setupMethods = getSecureBackupSetupMethods();
this._backupKey = null; if (setupMethods.includes(SecureBackupSetupMethod.Key)) {
passPhraseKeySelected = SecureBackupSetupMethod.Key;
} else {
passPhraseKeySelected = SecureBackupSetupMethod.Passphrase;
}
const accountPassword = props.accountPassword || "";
let canUploadKeysWithPasswordOnly = null;
if (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.
canUploadKeysWithPasswordOnly = true;
} else {
this.queryKeyUploadAuth();
}
this.state = { this.state = {
phase: PHASE_LOADING, phase: Phase.Loading,
passPhrase: '', passPhrase: '',
passPhraseValid: false, passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
@ -87,55 +138,37 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus: null, backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password // does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload? // for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null, accountPasswordCorrect: null,
canSkip: !isSecureBackupRequired(), canSkip: !isSecureBackupRequired(),
canUploadKeysWithPasswordOnly,
passPhraseKeySelected,
accountPassword,
}; };
const setupMethods = getSecureBackupSetupMethods(); MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
if (setupMethods.includes("key")) {
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY;
} else {
this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE;
}
this._passphraseField = createRef(); this.getInitialPhase();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
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();
}
this._getInitialPhase();
} }
componentWillUnmount() { public componentWillUnmount(): void {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatusChange);
} }
_getInitialPhase() { private getInitialPhase(): void {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
logger.log("Created key via customisations, jumping to bootstrap step"); logger.log("Created key via customisations, jumping to bootstrap step");
this._recoveryKey = { this.recoveryKey = {
privateKey: keyFromCustomisations, privateKey: keyFromCustomisations,
}; };
this._bootstrapSecretStorage(); this.bootstrapSecretStorage();
return; return;
} }
this._fetchBackupInfo(); this.fetchBackupInfo();
} }
async _fetchBackupInfo() { private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo, backupSigStatus: TrustInfo }> {
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = ( const backupSigStatus = (
@ -144,7 +177,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
); );
const { forceReset } = this.props; const { forceReset } = this.props;
const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; const phase = (backupInfo && !forceReset) ? Phase.Migrate : Phase.ChooseKeyPassphrase;
this.setState({ this.setState({
phase, phase,
@ -157,13 +190,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus, backupSigStatus,
}; };
} catch (e) { } catch (e) {
this.setState({ phase: PHASE_LOADERROR }); this.setState({ phase: Phase.LoadError });
} }
} }
async _queryKeyUploadAuth() { private async queryKeyUploadAuth(): Promise<void> {
try { try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys);
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
@ -182,59 +215,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
} }
_onKeyBackupStatusChange = () => { private onKeyBackupStatusChange = (): void => {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
} };
_onKeyPassphraseChange = e => { private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
passPhraseKeySelected: e.target.value, passPhraseKeySelected: e.target.value,
}); });
} };
_collectRecoveryKeyNode = (n) => { private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
this._recoveryKeyNode = n; if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
} this.recoveryKey =
_onChooseKeyPassphraseFormSubmit = async () => {
if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) {
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
setPassphrase: false, setPassphrase: false,
phase: PHASE_SHOWKEY, phase: Phase.ShowKey,
}); });
} else { } else {
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
phase: PHASE_PASSPHRASE, phase: Phase.Passphrase,
}); });
} }
} };
_onMigrateFormSubmit = (e) => { private onMigrateFormSubmit = (e: React.FormEvent): void => {
e.preventDefault(); e.preventDefault();
if (this.state.backupSigStatus.usable) { if (this.state.backupSigStatus.usable) {
this._bootstrapSecretStorage(); this.bootstrapSecretStorage();
} else { } else {
this._restoreBackup(); this.restoreBackup();
} }
} };
_onCopyClick = () => { private onCopyClick = (): void => {
const successful = copyNode(this._recoveryKeyNode); const successful = copyNode(this.recoveryKeyNode.current);
if (successful) { if (successful) {
this.setState({ this.setState({
copied: true, copied: true,
}); });
} }
} };
_onDownloadClick = () => { private onDownloadClick = (): void => {
const blob = new Blob([this._recoveryKey.encodedPrivateKey], { const blob = new Blob([this.recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'security-key.txt'); FileSaver.saveAs(blob, 'security-key.txt');
@ -242,9 +271,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({ this.setState({
downloaded: true, downloaded: true,
}); });
} };
_doBootstrapUIAuth = async (makeRequest) => { private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({ await makeRequest({
type: 'm.login.password', type: 'm.login.password',
@ -258,8 +287,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
password: this.state.accountPassword, password: this.state.accountPassword,
}); });
} else { } else {
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const dialogAesthetics = { const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), title: _t("Use Single Sign On to continue"),
@ -292,11 +319,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
throw new Error("Cross-signing key upload auth canceled"); throw new Error("Cross-signing key upload auth canceled");
} }
} }
} };
_bootstrapSecretStorage = async () => { private bootstrapSecretStorage = async (): Promise<void> => {
this.setState({ this.setState({
phase: PHASE_STORING, phase: Phase.Storing,
error: null, error: null,
}); });
@ -308,7 +335,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (forceReset) { if (forceReset) {
logger.log("Forcing secret storage reset"); logger.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this.recoveryKey,
setupNewKeyBackup: true, setupNewKeyBackup: true,
setupNewSecretStorage: true, setupNewSecretStorage: true,
}); });
@ -321,18 +348,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// keys (and also happen to skip all post-authentication flows at the // keys (and also happen to skip all post-authentication flows at the
// moment via token login) // moment via token login)
await cli.bootstrapCrossSigning({ await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth, authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
}); });
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this.recoveryKey,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo,
getKeyBackupPassphrase: () => { getKeyBackupPassphrase: async () => {
// We may already have the backup key if we earlier went // We may already have the backup key if we earlier went
// through the restore backup path, so pass it along // through the restore backup path, so pass it along
// rather than prompting again. // rather than prompting again.
if (this._backupKey) { if (this.backupKey) {
return this._backupKey; return this.backupKey;
} }
return promptForBackupPassphrase(); return promptForBackupPassphrase();
}, },
@ -344,27 +371,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.setState({ this.setState({
accountPassword: '', accountPassword: '',
accountPasswordCorrect: false, accountPasswordCorrect: false,
phase: PHASE_MIGRATE, phase: Phase.Migrate,
}); });
} else { } else {
this.setState({ error: e }); this.setState({ error: e });
} }
logger.error("Error bootstrapping secret storage", e); logger.error("Error bootstrapping secret storage", e);
} }
} };
_onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onDone = () => { private restoreBackup = async (): Promise<void> => {
this.props.onFinished(true);
}
_restoreBackup = async () => {
// It's possible we'll need the backup key later on for bootstrapping, // 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. // so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k; const keyCallback = k => this.backupKey = k;
const { finished } = Modal.createTrackedDialog( const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, 'Restore Backup', '', RestoreKeyBackupDialog,
@ -376,103 +399,103 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
); );
await finished; await finished;
const { backupSigStatus } = await this._fetchBackupInfo(); const { backupSigStatus } = await this.fetchBackupInfo();
if ( if (
backupSigStatus.usable && backupSigStatus.usable &&
this.state.canUploadKeysWithPasswordOnly && this.state.canUploadKeysWithPasswordOnly &&
this.state.accountPassword this.state.accountPassword
) { ) {
this._bootstrapSecretStorage(); this.bootstrapSecretStorage();
} }
} };
_onLoadRetryClick = () => { private onLoadRetryClick = (): void => {
this.setState({ phase: PHASE_LOADING }); this.setState({ phase: Phase.Loading });
this._fetchBackupInfo(); this.fetchBackupInfo();
} };
_onShowKeyContinueClick = () => { private onShowKeyContinueClick = (): void => {
this._bootstrapSecretStorage(); this.bootstrapSecretStorage();
} };
_onCancelClick = () => { private onCancelClick = (): void => {
this.setState({ phase: PHASE_CONFIRM_SKIP }); this.setState({ phase: Phase.ConfirmSkip });
} };
_onGoBackClick = () => { private onGoBackClick = (): void => {
this.setState({ phase: PHASE_CHOOSE_KEY_PASSPHRASE }); this.setState({ phase: Phase.ChooseKeyPassphrase });
} };
_onPassPhraseNextClick = async (e) => { private onPassPhraseNextClick = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!this._passphraseField.current) return; // unmounting if (!this.passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false }); await this.passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) { if (!this.passphraseField.current.state.valid) {
this._passphraseField.current.focus(); this.passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true }); this.passphraseField.current.validate({ allowEmpty: false, focused: true });
return; return;
} }
this.setState({ phase: PHASE_PASSPHRASE_CONFIRM }); this.setState({ phase: Phase.PassphraseConfirm });
}; };
_onPassPhraseConfirmNextClick = async (e) => { private onPassPhraseConfirmNextClick = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return; if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._recoveryKey = this.recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
setPassphrase: true, setPassphrase: true,
phase: PHASE_SHOWKEY, phase: Phase.ShowKey,
}); });
} };
_onSetAgainClick = () => { private onSetAgainClick = (): void => {
this.setState({ this.setState({
passPhrase: '', passPhrase: '',
passPhraseValid: false, passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
phase: PHASE_PASSPHRASE, phase: Phase.Passphrase,
}); });
} };
_onPassPhraseValidate = (result) => { private onPassPhraseValidate = (result: IValidationResult): void => {
this.setState({ this.setState({
passPhraseValid: result.valid, passPhraseValid: result.valid,
}); });
}; };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
} };
_onPassPhraseConfirmChange = (e) => { private onPassPhraseConfirmChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
passPhraseConfirm: e.target.value, passPhraseConfirm: e.target.value,
}); });
} };
_onAccountPasswordChange = (e) => { private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
accountPassword: e.target.value, accountPassword: e.target.value,
}); });
} };
_renderOptionKey() { private renderOptionKey(): JSX.Element {
return ( return (
<StyledRadioButton <StyledRadioButton
key={CREATE_STORAGE_OPTION_KEY} key={SecureBackupSetupMethod.Key}
value={CREATE_STORAGE_OPTION_KEY} value={SecureBackupSetupMethod.Key}
name="keyPassphrase" name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY} checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key}
onChange={this._onKeyPassphraseChange} onChange={this.onKeyPassphraseChange}
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
@ -484,14 +507,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
); );
} }
_renderOptionPassphrase() { private renderOptionPassphrase(): JSX.Element {
return ( return (
<StyledRadioButton <StyledRadioButton
key={CREATE_STORAGE_OPTION_PASSPHRASE} key={SecureBackupSetupMethod.Passphrase}
value={CREATE_STORAGE_OPTION_PASSPHRASE} value={SecureBackupSetupMethod.Passphrase}
name="keyPassphrase" name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE} checked={this.state.passPhraseKeySelected === SecureBackupSetupMethod.Passphrase}
onChange={this._onKeyPassphraseChange} onChange={this.onKeyPassphraseChange}
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
@ -503,12 +526,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
); );
} }
_renderPhaseChooseKeyPassphrase() { private renderPhaseChooseKeyPassphrase(): JSX.Element {
const setupMethods = getSecureBackupSetupMethods(); const setupMethods = getSecureBackupSetupMethods();
const optionKey = setupMethods.includes("key") ? this._renderOptionKey() : null; const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null;
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null; const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase)
? this.renderOptionPassphrase()
: null;
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}> return <form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t( <p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " + "Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.", "backing up encryption keys on your server.",
@ -519,20 +544,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Continue")} primaryButton={_t("Continue")}
onPrimaryButtonClick={this._onChooseKeyPassphraseFormSubmit} onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
hasCancel={this.state.canSkip} hasCancel={this.state.canSkip}
/> />
</form>; </form>;
} }
_renderPhaseMigrate() { private renderPhaseMigrate(): JSX.Element {
// TODO: This is a temporary screen so people who have the labs flag turned on and // 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. // 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 // Once we're confident enough in this (and it's supported enough) we can do
// it automatically. // it automatically.
// https://github.com/vector-im/element-web/issues/11696 // https://github.com/vector-im/element-web/issues/11696
const Field = sdk.getComponent('views.elements.Field');
let authPrompt; let authPrompt;
let nextCaption = _t("Next"); let nextCaption = _t("Next");
@ -543,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type="password" type="password"
label={_t("Password")} label={_t("Password")}
value={this.state.accountPassword} value={this.state.accountPassword}
onChange={this._onAccountPasswordChange} onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : null} forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true} autoFocus={true}
/></div> /></div>
@ -559,7 +583,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</p>; </p>;
} }
return <form onSubmit={this._onMigrateFormSubmit}> return <form onSubmit={this.onMigrateFormSubmit}>
<p>{ _t( <p>{ _t(
"Upgrade this session to allow it to verify other sessions, " + "Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " + "granting them access to encrypted messages and marking them " +
@ -568,19 +592,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div>{ authPrompt }</div> <div>{ authPrompt }</div>
<DialogButtons <DialogButtons
primaryButton={nextCaption} primaryButton={nextCaption}
onPrimaryButtonClick={this._onMigrateFormSubmit} onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false} hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
> >
<button type="button" className="danger" onClick={this._onCancelClick}> <button type="button" className="danger" onClick={this.onCancelClick}>
{ _t('Skip') } { _t('Skip') }
</button> </button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
_renderPhasePassPhrase() { private renderPhasePassPhrase(): JSX.Element {
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this.onPassPhraseNextClick}>
<p>{ _t( <p>{ _t(
"Enter a security phrase only you know, as its used to safeguard your data. " + "Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.", "To be secure, you shouldnt re-use your account password.",
@ -589,11 +613,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField <PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE} minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase} value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate} onValidate={this.onPassPhraseValidate}
fieldRef={this._passphraseField} fieldRef={this.passphraseField}
autoFocus={true} autoFocus={true}
label={_td("Enter a Security Phrase")} label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")} labelEnterPassword={_td("Enter a Security Phrase")}
@ -604,21 +628,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick} onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false} hasCancel={false}
disabled={!this.state.passPhraseValid} disabled={!this.state.passPhraseValid}
> >
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this.onCancelClick}
className="danger" className="danger"
>{ _t("Cancel") }</button> >{ _t("Cancel") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
_renderPhasePassPhraseConfirm() { private renderPhasePassPhraseConfirm(): JSX.Element {
const Field = sdk.getComponent('views.elements.Field');
let matchText; let matchText;
let changeText; let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) { if (this.state.passPhraseConfirm === this.state.passPhrase) {
@ -641,20 +663,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
passPhraseMatch = <div> passPhraseMatch = <div>
<div>{ matchText }</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this.onSetAgainClick}>
{ changeText } { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{ _t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
) }</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <Field
type="password" type="password"
onChange={this._onPassPhraseConfirmChange} onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your Security Phrase")} label={_t("Confirm your Security Phrase")}
@ -667,24 +689,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick} onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false} hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm} disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
> >
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this.onCancelClick}
className="danger" className="danger"
>{ _t("Skip") }</button> >{ _t("Skip") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
_renderPhaseShowKey() { private renderPhaseShowKey(): JSX.Element {
let continueButton; let continueButton;
if (this.state.phase === PHASE_SHOWKEY) { if (this.state.phase === Phase.ShowKey) {
continueButton = <DialogButtons primaryButton={_t("Continue")} continueButton = <DialogButtons primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase} disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
onPrimaryButtonClick={this._onShowKeyContinueClick} onPrimaryButtonClick={this.onShowKeyContinueClick}
hasCancel={false} hasCancel={false}
/>; />;
} else { } else {
@ -700,13 +722,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div className="mx_CreateSecretStorageDialog_primaryContainer"> <div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer"> <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey"> <div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code> <code ref={this.recoveryKeyNode}>{ this.recoveryKey.encodedPrivateKey }</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' <AccessibleButton kind='primary'
className="mx_Dialog_primary" className="mx_Dialog_primary"
onClick={this._onDownloadClick} onClick={this.onDownloadClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === Phase.Storing}
> >
{ _t("Download") } { _t("Download") }
</AccessibleButton> </AccessibleButton>
@ -714,8 +736,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<AccessibleButton <AccessibleButton
kind='primary' kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn" className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick} onClick={this.onCopyClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === Phase.Storing}
> >
{ this.state.copied ? _t("Copied!") : _t("Copy") } { this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton> </AccessibleButton>
@ -726,27 +748,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} }
_renderBusyPhase() { private renderBusyPhase(): JSX.Element {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div> return <div>
<Spinner /> <Spinner />
</div>; </div>;
} }
_renderPhaseLoadError() { private renderPhaseLoadError(): JSX.Element {
return <div> return <div>
<p>{ _t("Unable to query secret storage status") }</p> <p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick} onPrimaryButtonClick={this.onLoadRetryClick}
hasCancel={this.state.canSkip} hasCancel={this.state.canSkip}
onCancel={this._onCancel} onCancel={this.onCancel}
/> />
</div> </div>
</div>; </div>;
} }
_renderPhaseSkipConfirm() { private renderPhaseSkipConfirm(): JSX.Element {
return <div> return <div>
<p>{ _t( <p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
@ -755,98 +776,96 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
"You can also set up Secure Backup & manage your keys in Settings.", "You can also set up Secure Backup & manage your keys in Settings.",
) }</p> ) }</p>
<DialogButtons primaryButton={_t('Go back')} <DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick} onPrimaryButtonClick={this.onGoBackClick}
hasCancel={false} hasCancel={false}
> >
<button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button> <button type="button" className="danger" onClick={this.onCancel}>{ _t('Cancel') }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
_titleForPhase(phase) { private titleForPhase(phase: Phase): string {
switch (phase) { switch (phase) {
case PHASE_CHOOSE_KEY_PASSPHRASE: case Phase.ChooseKeyPassphrase:
return _t('Set up Secure Backup'); return _t('Set up Secure Backup');
case PHASE_MIGRATE: case Phase.Migrate:
return _t('Upgrade your encryption'); return _t('Upgrade your encryption');
case PHASE_PASSPHRASE: case Phase.Passphrase:
return _t('Set a Security Phrase'); return _t('Set a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM: case Phase.PassphraseConfirm:
return _t('Confirm Security Phrase'); return _t('Confirm Security Phrase');
case PHASE_CONFIRM_SKIP: case Phase.ConfirmSkip:
return _t('Are you sure?'); return _t('Are you sure?');
case PHASE_SHOWKEY: case Phase.ShowKey:
return _t('Save your Security Key'); return _t('Save your Security Key');
case PHASE_STORING: case Phase.Storing:
return _t('Setting up keys'); return _t('Setting up keys');
default: default:
return ''; return '';
} }
} }
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let content; let content;
if (this.state.error) { if (this.state.error) {
content = <div> content = <div>
<p>{ _t("Unable to set up secret storage") }</p> <p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage} onPrimaryButtonClick={this.bootstrapSecretStorage}
hasCancel={this.state.canSkip} hasCancel={this.state.canSkip}
onCancel={this._onCancel} onCancel={this.onCancel}
/> />
</div> </div>
</div>; </div>;
} else { } else {
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_LOADING: case Phase.Loading:
content = this._renderBusyPhase(); content = this.renderBusyPhase();
break; break;
case PHASE_LOADERROR: case Phase.LoadError:
content = this._renderPhaseLoadError(); content = this.renderPhaseLoadError();
break; break;
case PHASE_CHOOSE_KEY_PASSPHRASE: case Phase.ChooseKeyPassphrase:
content = this._renderPhaseChooseKeyPassphrase(); content = this.renderPhaseChooseKeyPassphrase();
break; break;
case PHASE_MIGRATE: case Phase.Migrate:
content = this._renderPhaseMigrate(); content = this.renderPhaseMigrate();
break; break;
case PHASE_PASSPHRASE: case Phase.Passphrase:
content = this._renderPhasePassPhrase(); content = this.renderPhasePassPhrase();
break; break;
case PHASE_PASSPHRASE_CONFIRM: case Phase.PassphraseConfirm:
content = this._renderPhasePassPhraseConfirm(); content = this.renderPhasePassPhraseConfirm();
break; break;
case PHASE_SHOWKEY: case Phase.ShowKey:
content = this._renderPhaseShowKey(); content = this.renderPhaseShowKey();
break; break;
case PHASE_STORING: case Phase.Storing:
content = this._renderBusyPhase(); content = this.renderBusyPhase();
break; break;
case PHASE_CONFIRM_SKIP: case Phase.ConfirmSkip:
content = this._renderPhaseSkipConfirm(); content = this.renderPhaseSkipConfirm();
break; break;
} }
} }
let titleClass = null; let titleClass = null;
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_PASSPHRASE: case Phase.Passphrase:
case PHASE_PASSPHRASE_CONFIRM: case Phase.PassphraseConfirm:
titleClass = [ titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_securePhraseTitle', 'mx_CreateSecretStorageDialog_securePhraseTitle',
]; ];
break; break;
case PHASE_SHOWKEY: case Phase.ShowKey:
titleClass = [ titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_secureBackupTitle', 'mx_CreateSecretStorageDialog_secureBackupTitle',
]; ];
break; break;
case PHASE_CHOOSE_KEY_PASSPHRASE: case Phase.ChooseKeyPassphrase:
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
break; break;
} }
@ -854,9 +873,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return ( return (
<BaseDialog className='mx_CreateSecretStorageDialog' <BaseDialog className='mx_CreateSecretStorageDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)} title={this.titleForPhase(this.state.phase)}
titleClass={titleClass} titleClass={titleClass}
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} hasCancel={this.props.hasCancel && [Phase.Passphrase].includes(this.state.phase)}
fixedWidth={false} fixedWidth={false}
> >
<div> <div>

View file

@ -16,47 +16,51 @@ limitations under the License.
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index'; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
const PHASE_EDIT = 1; enum Phase {
const PHASE_EXPORTING = 2; Edit = "edit",
Exporting = "exporting",
}
export default class ExportE2eKeysDialog extends React.Component { interface IProps extends IDialogProps {
static propTypes = { matrixClient: MatrixClient;
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, }
onFinished: PropTypes.func.isRequired,
};
constructor(props) { interface IState {
phase: Phase;
errStr: string;
}
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false;
private passphrase1 = createRef<HTMLInputElement>();
private passphrase2 = createRef<HTMLInputElement>();
constructor(props: IProps) {
super(props); super(props);
this._unmounted = false;
this._passphrase1 = createRef();
this._passphrase2 = createRef();
this.state = { this.state = {
phase: PHASE_EDIT, phase: Phase.Edit,
errStr: null, errStr: null,
}; };
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
} }
_onPassphraseFormSubmit = (ev) => { private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
const passphrase = this._passphrase1.current.value; const passphrase = this.passphrase1.current.value;
if (passphrase !== this._passphrase2.current.value) { if (passphrase !== this.passphrase2.current.value) {
this.setState({ errStr: _t('Passphrases must match') }); this.setState({ errStr: _t('Passphrases must match') });
return false; return false;
} }
@ -65,11 +69,11 @@ export default class ExportE2eKeysDialog extends React.Component {
return false; return false;
} }
this._startExport(passphrase); this.startExport(passphrase);
return false; return false;
}; };
_startExport(passphrase) { private startExport(passphrase: string): void {
// extra Promise.resolve() to turn synchronous exceptions into // extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones. // asynchronous ones.
Promise.resolve().then(() => { Promise.resolve().then(() => {
@ -86,39 +90,37 @@ export default class ExportE2eKeysDialog extends React.Component {
this.props.onFinished(true); this.props.onFinished(true);
}).catch((e) => { }).catch((e) => {
logger.error("Error exporting e2e keys:", e); logger.error("Error exporting e2e keys:", e);
if (this._unmounted) { if (this.unmounted) {
return; return;
} }
const msg = e.friendlyText || _t('Unknown error'); const msg = e.friendlyText || _t('Unknown error');
this.setState({ this.setState({
errStr: msg, errStr: msg,
phase: PHASE_EDIT, phase: Phase.Edit,
}); });
}); });
this.setState({ this.setState({
errStr: null, errStr: null,
phase: PHASE_EXPORTING, phase: Phase.Exporting,
}); });
} }
_onCancelClick = (ev) => { private onCancelClick = (ev: React.MouseEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
this.props.onFinished(false); this.props.onFinished(false);
return false; return false;
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const disableForm = (this.state.phase === Phase.Exporting);
const disableForm = (this.state.phase === PHASE_EXPORTING);
return ( return (
<BaseDialog className='mx_exportE2eKeysDialog' <BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Export room keys")} title={_t("Export room keys")}
> >
<form onSubmit={this._onPassphraseFormSubmit}> <form onSubmit={this.onPassphraseFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
{ _t( { _t(
@ -151,10 +153,10 @@ export default class ExportE2eKeysDialog extends React.Component {
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input <input
ref={this._passphrase1} ref={this.passphrase1}
id='passphrase1' id='passphrase1'
autoFocus={true} autoFocus={true}
size='64' size={64}
type='password' type='password'
disabled={disableForm} disabled={disableForm}
/> />
@ -167,9 +169,9 @@ export default class ExportE2eKeysDialog extends React.Component {
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase2} <input ref={this.passphrase2}
id='passphrase2' id='passphrase2'
size='64' size={64}
type='password' type='password'
disabled={disableForm} disabled={disableForm}
/> />
@ -184,7 +186,7 @@ export default class ExportE2eKeysDialog extends React.Component {
value={_t('Export')} value={_t('Export')}
disabled={disableForm} disabled={disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") } { _t("Cancel") }
</button> </button>
</div> </div>

View file

@ -15,20 +15,19 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import * as sdk from '../../../../index';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
function readFileAsArrayBuffer(file) { function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
resolve(e.target.result); resolve(e.target.result as ArrayBuffer);
}; };
reader.onerror = reject; reader.onerror = reject;
@ -36,51 +35,57 @@ function readFileAsArrayBuffer(file) {
}); });
} }
const PHASE_EDIT = 1; enum Phase {
const PHASE_IMPORTING = 2; Edit = "edit",
Importing = "importing",
}
export default class ImportE2eKeysDialog extends React.Component { interface IProps extends IDialogProps {
static propTypes = { matrixClient: MatrixClient;
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, }
onFinished: PropTypes.func.isRequired,
};
constructor(props) { interface IState {
enableSubmit: boolean;
phase: Phase;
errStr: string;
}
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
private unmounted = false;
private file = createRef<HTMLInputElement>();
private passphrase = createRef<HTMLInputElement>();
constructor(props: IProps) {
super(props); super(props);
this._unmounted = false;
this._file = createRef();
this._passphrase = createRef();
this.state = { this.state = {
enableSubmit: false, enableSubmit: false,
phase: PHASE_EDIT, phase: Phase.Edit,
errStr: null, errStr: null,
}; };
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
} }
_onFormChange = (ev) => { private onFormChange = (ev: React.FormEvent): void => {
const files = this._file.current.files || []; const files = this.file.current.files || [];
this.setState({ this.setState({
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0), enableSubmit: (this.passphrase.current.value !== "" && files.length > 0),
}); });
}; };
_onFormSubmit = (ev) => { private onFormSubmit = (ev: React.FormEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
this._startImport(this._file.current.files[0], this._passphrase.current.value); this.startImport(this.file.current.files[0], this.passphrase.current.value);
return false; return false;
}; };
_startImport(file, passphrase) { private startImport(file: File, passphrase: string) {
this.setState({ this.setState({
errStr: null, errStr: null,
phase: PHASE_IMPORTING, phase: Phase.Importing,
}); });
return readFileAsArrayBuffer(file).then((arrayBuffer) => { return readFileAsArrayBuffer(file).then((arrayBuffer) => {
@ -94,34 +99,32 @@ export default class ImportE2eKeysDialog extends React.Component {
this.props.onFinished(true); this.props.onFinished(true);
}).catch((e) => { }).catch((e) => {
logger.error("Error importing e2e keys:", e); logger.error("Error importing e2e keys:", e);
if (this._unmounted) { if (this.unmounted) {
return; return;
} }
const msg = e.friendlyText || _t('Unknown error'); const msg = e.friendlyText || _t('Unknown error');
this.setState({ this.setState({
errStr: msg, errStr: msg,
phase: PHASE_EDIT, phase: Phase.Edit,
}); });
}); });
} }
_onCancelClick = (ev) => { private onCancelClick = (ev: React.MouseEvent): boolean => {
ev.preventDefault(); ev.preventDefault();
this.props.onFinished(false); this.props.onFinished(false);
return false; return false;
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const disableForm = (this.state.phase !== Phase.Edit);
const disableForm = (this.state.phase !== PHASE_EDIT);
return ( return (
<BaseDialog className='mx_importE2eKeysDialog' <BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Import room keys")} title={_t("Import room keys")}
> >
<form onSubmit={this._onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
{ _t( { _t(
@ -149,11 +152,11 @@ export default class ImportE2eKeysDialog extends React.Component {
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input <input
ref={this._file} ref={this.file}
id='importFile' id='importFile'
type='file' type='file'
autoFocus={true} autoFocus={true}
onChange={this._onFormChange} onChange={this.onFormChange}
disabled={disableForm} /> disabled={disableForm} />
</div> </div>
</div> </div>
@ -165,11 +168,11 @@ export default class ImportE2eKeysDialog extends React.Component {
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
<input <input
ref={this._passphrase} ref={this.passphrase}
id='passphrase' id='passphrase'
size='64' size={64}
type='password' type='password'
onChange={this._onFormChange} onChange={this.onFormChange}
disabled={disableForm} /> disabled={disableForm} />
</div> </div>
</div> </div>
@ -182,7 +185,7 @@ export default class ImportE2eKeysDialog extends React.Component {
value={_t('Import')} value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") } { _t("Cancel") }
</button> </button>
</div> </div>

View file

@ -16,43 +16,40 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
export default class NewRecoveryMethodDialog extends React.PureComponent { interface IProps extends IDialogProps {
static propTypes = { newVersionInfo: IKeyBackupInfo;
// As returned by js-sdk getKeyBackupVersion() }
newVersionInfo: PropTypes.object,
onFinished: PropTypes.func.isRequired,
}
onOkClick = () => { export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {
private onOkClick = (): void => {
this.props.onFinished(); this.props.onFinished();
} };
onGoToSettingsClick = () => { private onGoToSettingsClick = (): void => {
this.props.onFinished(); this.props.onFinished();
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
} };
onSetupClick = async () => { private onSetupClick = async (): Promise<void> => {
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, { 'Restore Backup', '', RestoreKeyBackupDialog, {
onFinished: this.props.onFinished, onFinished: this.props.onFinished,
}, null, /* priority = */ false, /* static = */ true, }, null, /* priority = */ false, /* static = */ true,
); );
} };
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("New Recovery Method") } { _t("New Recovery Method") }
</span>; </span>;

View file

@ -15,36 +15,32 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ComponentType } from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../../index";
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
export default class RecoveryMethodRemovedDialog extends React.PureComponent { interface IProps extends IDialogProps {}
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
onGoToSettingsClick = () => { export default class RecoveryMethodRemovedDialog extends React.PureComponent<IProps> {
private onGoToSettingsClick = (): void => {
this.props.onFinished(); this.props.onFinished();
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
} };
onSetupClick = () => { private onSetupClick = (): void => {
this.props.onFinished(); this.props.onFinished();
Modal.createTrackedDialogAsync("Key Backup", "Key Backup", Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("./CreateKeyBackupDialog"), import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>,
null, null, /* priority = */ false, /* static = */ true, null, null, /* priority = */ false, /* static = */ true,
); );
} };
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("Recovery Method Removed") } { _t("Recovery Method Removed") }
</span>; </span>;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { ComponentType, createRef } from 'react';
import { createClient } from "matrix-js-sdk/src/matrix"; import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -1601,12 +1601,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (haveNewVersion) { if (haveNewVersion) {
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'), import(
'../../async-components/views/dialogs/security/NewRecoveryMethodDialog'
) as unknown as Promise<ComponentType<{}>>,
{ newVersionInfo }, { newVersionInfo },
); );
} else { } else {
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'), import(
'../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'
) as unknown as Promise<ComponentType<{}>>,
); );
} }
}); });

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ComponentType } from 'react';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
@ -85,7 +85,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '', Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
}, },
@ -111,7 +113,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
); );
} else { } else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup", Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"), import(
"../../../async-components/views/dialogs/security/CreateKeyBackupDialog"
) as unknown as Promise<ComponentType<{}>>,
null, null, /* priority = */ false, /* static = */ true, null, null, /* priority = */ false, /* static = */ true,
); );
} }

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import Field from "../elements/Field"; import Field from "../elements/Field";
import React from 'react'; import React, { ComponentType } from 'react';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner'; import Spinner from '../elements/Spinner';
@ -186,7 +186,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
}, },

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ComponentType } from 'react';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -92,14 +92,18 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '', Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ matrixClient: MatrixClientPeg.get() }, { matrixClient: MatrixClientPeg.get() },
); );
}; };
private onImportE2eKeysClicked = (): void => { private onImportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Import E2E Keys', '', Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../async-components/views/dialogs/security/ImportE2eKeysDialog'), import(
'../../../async-components/views/dialogs/security/ImportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ matrixClient: MatrixClientPeg.get() }, { matrixClient: MatrixClientPeg.get() },
); );
}; };

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ComponentType } from 'react';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -170,7 +170,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private startNewBackup = (): void => { private startNewBackup = (): void => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'), import(
'../../../async-components/views/dialogs/security/CreateKeyBackupDialog'
) as unknown as Promise<ComponentType<{}>>,
{ {
onFinished: () => { onFinished: () => {
this.loadBackupStatus(); this.loadBackupStatus();