mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-25 18:08:14 +08:00
Remove "Upgrade your encryption" flow in CreateSecretStorageDialog
(#28290)
* Remove "Upgrade your encryption" flow * Rename and remove tests * Remove `BackupTrustInfo` * Get keybackup when bootstraping the secret storage. * Update src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
c23c9dfacb
commit
386b782f2a
@ -11,7 +11,7 @@ import React, { createRef } from "react";
|
|||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||||
import { CryptoEvent, BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import CheckmarkIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
import CheckmarkIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||||
|
|
||||||
@ -25,7 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio
|
|||||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||||
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 {
|
import {
|
||||||
getSecureBackupSetupMethods,
|
getSecureBackupSetupMethods,
|
||||||
isSecureBackupRequired,
|
isSecureBackupRequired,
|
||||||
@ -45,7 +44,6 @@ enum Phase {
|
|||||||
Loading = "loading",
|
Loading = "loading",
|
||||||
LoadError = "load_error",
|
LoadError = "load_error",
|
||||||
ChooseKeyPassphrase = "choose_key_passphrase",
|
ChooseKeyPassphrase = "choose_key_passphrase",
|
||||||
Migrate = "migrate",
|
|
||||||
Passphrase = "passphrase",
|
Passphrase = "passphrase",
|
||||||
PassphraseConfirm = "passphrase_confirm",
|
PassphraseConfirm = "passphrase_confirm",
|
||||||
ShowKey = "show_key",
|
ShowKey = "show_key",
|
||||||
@ -72,24 +70,6 @@ interface IState {
|
|||||||
downloaded: boolean;
|
downloaded: boolean;
|
||||||
setPassphrase: boolean;
|
setPassphrase: boolean;
|
||||||
|
|
||||||
/** Information on the current key backup version, as returned by the server.
|
|
||||||
*
|
|
||||||
* `null` could mean any of:
|
|
||||||
* * we haven't yet requested the data from the server.
|
|
||||||
* * we were unable to reach the server.
|
|
||||||
* * the server returned key backup version data we didn't understand or was malformed.
|
|
||||||
* * there is actually no backup on the server.
|
|
||||||
*/
|
|
||||||
backupInfo: KeyBackupInfo | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
|
|
||||||
* decrypt it.
|
|
||||||
*
|
|
||||||
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
|
|
||||||
*/
|
|
||||||
backupTrustInfo: BackupTrustInfo | undefined;
|
|
||||||
|
|
||||||
// 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: boolean | null;
|
canUploadKeysWithPasswordOnly: boolean | null;
|
||||||
@ -141,16 +121,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
this.queryKeyUploadAuth();
|
this.queryKeyUploadAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
||||||
|
const phase = keyFromCustomisations ? Phase.Loading : Phase.ChooseKeyPassphrase;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: Phase.Loading,
|
phase,
|
||||||
passPhrase: "",
|
passPhrase: "",
|
||||||
passPhraseValid: false,
|
passPhraseValid: false,
|
||||||
passPhraseConfirm: "",
|
passPhraseConfirm: "",
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
setPassphrase: false,
|
setPassphrase: false,
|
||||||
backupInfo: null,
|
|
||||||
backupTrustInfo: undefined,
|
|
||||||
// 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?
|
||||||
accountPasswordCorrect: null,
|
accountPasswordCorrect: null,
|
||||||
@ -160,60 +141,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
accountPassword,
|
accountPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
cli.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
if (keyFromCustomisations) this.initExtension(keyFromCustomisations);
|
||||||
|
|
||||||
this.getInitialPhase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
private initExtension(keyFromCustomisations: Uint8Array): void {
|
||||||
MatrixClientPeg.get()?.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInitialPhase(): void {
|
|
||||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.createSecretStorageKey();
|
|
||||||
if (keyFromCustomisations) {
|
|
||||||
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step");
|
||||||
this.recoveryKey = {
|
this.recoveryKey = {
|
||||||
privateKey: keyFromCustomisations,
|
privateKey: keyFromCustomisations,
|
||||||
};
|
};
|
||||||
this.bootstrapSecretStorage();
|
this.bootstrapSecretStorage();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchBackupInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to get information on the current backup from the server, and update the state.
|
|
||||||
*
|
|
||||||
* Updates {@link IState.backupInfo} and {@link IState.backupTrustInfo}, and picks an appropriate phase for
|
|
||||||
* {@link IState.phase}.
|
|
||||||
*
|
|
||||||
* @returns If the backup data was retrieved successfully, the trust info for the backup. Otherwise, undefined.
|
|
||||||
*/
|
|
||||||
private async fetchBackupInfo(): Promise<BackupTrustInfo | undefined> {
|
|
||||||
try {
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const backupInfo = await cli.getKeyBackupVersion();
|
|
||||||
const backupTrustInfo =
|
|
||||||
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
|
||||||
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
|
||||||
|
|
||||||
const { forceReset } = this.props;
|
|
||||||
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase,
|
|
||||||
backupInfo,
|
|
||||||
backupTrustInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return backupTrustInfo;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error fetching backup data from server", e);
|
|
||||||
this.setState({ phase: Phase.LoadError });
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async queryKeyUploadAuth(): Promise<void> {
|
private async queryKeyUploadAuth(): Promise<void> {
|
||||||
@ -237,10 +173,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyBackupStatusChange = (): void => {
|
|
||||||
if (this.state.phase === Phase.Migrate) this.fetchBackupInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onKeyPassphraseChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhraseKeySelected: e.target.value,
|
passPhraseKeySelected: e.target.value,
|
||||||
@ -265,15 +197,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.state.backupTrustInfo?.trusted) {
|
|
||||||
this.bootstrapSecretStorage();
|
|
||||||
} else {
|
|
||||||
this.restoreBackup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCopyClick = (): void => {
|
private onCopyClick = (): void => {
|
||||||
const successful = copyNode(this.recoveryKeyNode.current);
|
const successful = copyNode(this.recoveryKeyNode.current);
|
||||||
if (successful) {
|
if (successful) {
|
||||||
@ -340,16 +263,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||||
|
const cli = MatrixClientPeg.safeGet();
|
||||||
|
const crypto = cli.getCrypto()!;
|
||||||
|
const { forceReset } = this.props;
|
||||||
|
|
||||||
|
let backupInfo;
|
||||||
|
// First, unless we know we want to do a reset, we see if there is an existing key backup
|
||||||
|
if (!forceReset) {
|
||||||
|
try {
|
||||||
|
this.setState({ phase: Phase.Loading });
|
||||||
|
backupInfo = await cli.getKeyBackupVersion();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error fetching backup data from server", e);
|
||||||
|
this.setState({ phase: Phase.LoadError });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: Phase.Storing,
|
phase: Phase.Storing,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const crypto = cli.getCrypto()!;
|
|
||||||
|
|
||||||
const { forceReset } = this.props;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (forceReset) {
|
if (forceReset) {
|
||||||
logger.log("Forcing secret storage reset");
|
logger.log("Forcing secret storage reset");
|
||||||
@ -371,8 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
});
|
});
|
||||||
await crypto.bootstrapSecretStorage({
|
await crypto.bootstrapSecretStorage({
|
||||||
createSecretStorageKey: async () => this.recoveryKey!,
|
createSecretStorageKey: async () => this.recoveryKey!,
|
||||||
keyBackupInfo: this.state.backupInfo!,
|
setupNewKeyBackup: !backupInfo,
|
||||||
setupNewKeyBackup: !this.state.backupInfo,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await initialiseDehydration(true);
|
await initialiseDehydration(true);
|
||||||
@ -381,20 +315,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
phase: Phase.Stored,
|
phase: Phase.Stored,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
|
||||||
this.state.canUploadKeysWithPasswordOnly &&
|
|
||||||
e instanceof MatrixError &&
|
|
||||||
e.httpStatus === 401 &&
|
|
||||||
e.data.flows
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
accountPassword: "",
|
|
||||||
accountPasswordCorrect: false,
|
|
||||||
phase: Phase.Migrate,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
}
|
|
||||||
logger.error("Error bootstrapping secret storage", e);
|
logger.error("Error bootstrapping secret storage", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -403,27 +324,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
private restoreBackup = async (): Promise<void> => {
|
|
||||||
const { finished } = Modal.createDialog(
|
|
||||||
RestoreKeyBackupDialog,
|
|
||||||
{
|
|
||||||
showSummary: false,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
/* priority = */ false,
|
|
||||||
/* static = */ false,
|
|
||||||
);
|
|
||||||
|
|
||||||
await finished;
|
|
||||||
const backupTrustInfo = await this.fetchBackupInfo();
|
|
||||||
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
|
||||||
this.bootstrapSecretStorage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onLoadRetryClick = (): void => {
|
private onLoadRetryClick = (): void => {
|
||||||
this.setState({ phase: Phase.Loading });
|
this.bootstrapSecretStorage();
|
||||||
this.fetchBackupInfo();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowKeyContinueClick = (): void => {
|
private onShowKeyContinueClick = (): void => {
|
||||||
@ -495,12 +397,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAccountPasswordChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
this.setState({
|
|
||||||
accountPassword: e.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderOptionKey(): JSX.Element {
|
private renderOptionKey(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
@ -565,55 +461,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPhaseMigrate(): JSX.Element {
|
|
||||||
let authPrompt;
|
|
||||||
let nextCaption = _t("action|next");
|
|
||||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
|
||||||
authPrompt = (
|
|
||||||
<div>
|
|
||||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}</div>
|
|
||||||
<div>
|
|
||||||
<Field
|
|
||||||
id="mx_CreateSecretStorageDialog_password"
|
|
||||||
type="password"
|
|
||||||
label={_t("common|password")}
|
|
||||||
value={this.state.accountPassword}
|
|
||||||
onChange={this.onAccountPasswordChange}
|
|
||||||
forceValidity={this.state.accountPasswordCorrect === false ? false : undefined}
|
|
||||||
autoFocus={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (!this.state.backupTrustInfo?.trusted) {
|
|
||||||
authPrompt = (
|
|
||||||
<div>
|
|
||||||
<div>{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
nextCaption = _t("action|restore");
|
|
||||||
} else {
|
|
||||||
authPrompt = <p>{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={this.onMigrateFormSubmit}>
|
|
||||||
<p>{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}</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("action|skip")}
|
|
||||||
</button>
|
|
||||||
</DialogButtons>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPhasePassPhrase(): JSX.Element {
|
private renderPhasePassPhrase(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onPassPhraseNextClick}>
|
<form onSubmit={this.onPassPhraseNextClick}>
|
||||||
@ -829,8 +676,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
switch (phase) {
|
switch (phase) {
|
||||||
case Phase.ChooseKeyPassphrase:
|
case Phase.ChooseKeyPassphrase:
|
||||||
return _t("encryption|set_up_toast_title");
|
return _t("encryption|set_up_toast_title");
|
||||||
case Phase.Migrate:
|
|
||||||
return _t("settings|key_backup|setup_secure_backup|title_upgrade_encryption");
|
|
||||||
case Phase.Passphrase:
|
case Phase.Passphrase:
|
||||||
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
|
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
|
||||||
case Phase.PassphraseConfirm:
|
case Phase.PassphraseConfirm:
|
||||||
@ -889,9 +734,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||||||
case Phase.ChooseKeyPassphrase:
|
case Phase.ChooseKeyPassphrase:
|
||||||
content = this.renderPhaseChooseKeyPassphrase();
|
content = this.renderPhaseChooseKeyPassphrase();
|
||||||
break;
|
break;
|
||||||
case Phase.Migrate:
|
|
||||||
content = this.renderPhaseMigrate();
|
|
||||||
break;
|
|
||||||
case Phase.Passphrase:
|
case Phase.Passphrase:
|
||||||
content = this.renderPhasePassPhrase();
|
content = this.renderPhasePassPhrase();
|
||||||
break;
|
break;
|
||||||
|
@ -103,7 +103,6 @@
|
|||||||
"report_content": "Report Content",
|
"report_content": "Report Content",
|
||||||
"resend": "Resend",
|
"resend": "Resend",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"restore": "Restore",
|
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"review": "Review",
|
"review": "Review",
|
||||||
@ -2587,18 +2586,13 @@
|
|||||||
"pass_phrase_match_failed": "That doesn't match.",
|
"pass_phrase_match_failed": "That doesn't match.",
|
||||||
"pass_phrase_match_success": "That matches!",
|
"pass_phrase_match_success": "That matches!",
|
||||||
"phrase_strong_enough": "Great! This Security Phrase looks strong enough.",
|
"phrase_strong_enough": "Great! This Security Phrase looks strong enough.",
|
||||||
"requires_key_restore": "Restore your key backup to upgrade your encryption",
|
|
||||||
"requires_password_confirmation": "Enter your account password to confirm the upgrade:",
|
|
||||||
"requires_server_authentication": "You'll need to authenticate with the server to confirm the upgrade.",
|
|
||||||
"secret_storage_query_failure": "Unable to query secret storage status",
|
"secret_storage_query_failure": "Unable to query secret storage status",
|
||||||
"security_key_safety_reminder": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.",
|
"security_key_safety_reminder": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.",
|
||||||
"session_upgrade_description": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
|
|
||||||
"set_phrase_again": "Go back to set it again.",
|
"set_phrase_again": "Go back to set it again.",
|
||||||
"settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.",
|
"settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.",
|
||||||
"title_confirm_phrase": "Confirm Security Phrase",
|
"title_confirm_phrase": "Confirm Security Phrase",
|
||||||
"title_save_key": "Save your Security Key",
|
"title_save_key": "Save your Security Key",
|
||||||
"title_set_phrase": "Set a Security Phrase",
|
"title_set_phrase": "Set a Security Phrase",
|
||||||
"title_upgrade_encryption": "Upgrade your encryption",
|
|
||||||
"unable_to_setup": "Unable to set up secret storage",
|
"unable_to_setup": "Unable to set up secret storage",
|
||||||
"use_different_passphrase": "Use a different passphrase?",
|
"use_different_passphrase": "Use a different passphrase?",
|
||||||
"use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Security Key to use for backup."
|
"use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Security Key to use for backup."
|
||||||
|
@ -127,6 +127,10 @@ export function createTestClient(): MatrixClient {
|
|||||||
prepareToEncrypt: jest.fn(),
|
prepareToEncrypt: jest.fn(),
|
||||||
bootstrapCrossSigning: jest.fn(),
|
bootstrapCrossSigning: jest.fn(),
|
||||||
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
||||||
|
isKeyBackupTrusted: jest.fn().mockResolvedValue({}),
|
||||||
|
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({}),
|
||||||
|
bootstrapSecretStorage: jest.fn(),
|
||||||
|
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: jest.fn(),
|
||||||
@ -270,6 +274,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
getOrCreateFilter: jest.fn(),
|
getOrCreateFilter: jest.fn(),
|
||||||
sendStickerMessage: jest.fn(),
|
sendStickerMessage: jest.fn(),
|
||||||
getLocalAliases: jest.fn().mockReturnValue([]),
|
getLocalAliases: jest.fn().mockReturnValue([]),
|
||||||
|
uploadDeviceSigningKeys: jest.fn(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
@ -10,42 +10,23 @@ import { render, RenderResult, screen } from "jest-matrix-react";
|
|||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { mocked, MockedObject } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
import { Crypto, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||||
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import {
|
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||||
filterConsole,
|
|
||||||
flushPromises,
|
|
||||||
getMockClientWithEventEmitter,
|
|
||||||
mockClientMethodsCrypto,
|
|
||||||
mockClientMethodsServer,
|
|
||||||
} from "../../../../../test-utils";
|
|
||||||
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
|
import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
|
||||||
import Modal from "../../../../../../src/Modal";
|
|
||||||
import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog";
|
|
||||||
|
|
||||||
describe("CreateSecretStorageDialog", () => {
|
describe("CreateSecretStorageDialog", () => {
|
||||||
let mockClient: MockedObject<MatrixClient>;
|
let mockClient: MockedObject<MatrixClient>;
|
||||||
let mockCrypto: MockedObject<Crypto.CryptoApi>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = mocked(stubClient());
|
||||||
...mockClientMethodsServer(),
|
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
|
||||||
...mockClientMethodsCrypto(),
|
|
||||||
uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => {
|
|
||||||
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
|
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
|
||||||
throw new MatrixError({ flows: [] });
|
throw new MatrixError({ flows: [] });
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
|
||||||
Object.assign(mockCrypto, {
|
|
||||||
isKeyBackupTrusted: jest.fn(),
|
|
||||||
isDehydrationSupported: jest.fn(() => false),
|
|
||||||
bootstrapCrossSigning: jest.fn(),
|
|
||||||
bootstrapSecretStorage: jest.fn(),
|
|
||||||
});
|
});
|
||||||
|
// Mock the clipboard API
|
||||||
|
document.execCommand = jest.fn().mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -59,11 +40,37 @@ describe("CreateSecretStorageDialog", () => {
|
|||||||
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
|
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
it("shows a loading spinner initially", async () => {
|
it("handles the happy path", async () => {
|
||||||
const { container } = renderComponent();
|
const result = renderComponent();
|
||||||
expect(screen.getByTestId("spinner")).toBeDefined();
|
await result.findByText(
|
||||||
expect(container).toMatchSnapshot();
|
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
|
||||||
await flushPromises();
|
);
|
||||||
|
expect(result.container).toMatchSnapshot();
|
||||||
|
await userEvent.click(result.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await screen.findByText("Save your Security Key");
|
||||||
|
expect(result.container).toMatchSnapshot();
|
||||||
|
// Copy the key to enable the continue button
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||||
|
expect(result.queryByText("Copied!")).not.toBeNull();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await screen.findByText("Your keys are now being backed up from this device.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when there is an error when bootstraping the secret storage, it shows an error", async () => {
|
||||||
|
jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockRejectedValue(new Error("error"));
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
await screen.findByText(
|
||||||
|
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
await screen.findByText("Save your Security Key");
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await screen.findByText("Unable to set up secret storage");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there is an error fetching the backup version", () => {
|
describe("when there is an error fetching the backup version", () => {
|
||||||
@ -75,139 +82,19 @@ describe("CreateSecretStorageDialog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = renderComponent();
|
const result = renderComponent();
|
||||||
|
// We go though the dialog until we have to get the key backup
|
||||||
|
await userEvent.click(result.getByRole("button", { name: "Continue" }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Copy" }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
// XXX the error message is... misleading.
|
// XXX the error message is... misleading.
|
||||||
await result.findByText("Unable to query secret storage status");
|
await screen.findByText("Unable to query secret storage status");
|
||||||
expect(result.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows 'Generate a Security Key' text if no key backup is present", async () => {
|
|
||||||
const result = renderComponent();
|
|
||||||
await flushPromises();
|
|
||||||
expect(result.container).toMatchSnapshot();
|
|
||||||
result.getByText("Generate a Security Key");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when canUploadKeysWithPasswordOnly", () => {
|
|
||||||
// spy on Modal.createDialog
|
|
||||||
let modalSpy: jest.SpyInstance;
|
|
||||||
|
|
||||||
// deferred which should be resolved to indicate that the created dialog has completed
|
|
||||||
let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
|
||||||
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
|
|
||||||
await sleep(0);
|
|
||||||
throw new MatrixError({
|
|
||||||
flows: [{ stages: ["m.login.password"] }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
|
||||||
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
|
||||||
finished: restoreDialogFinishedDefer.promise,
|
|
||||||
close: jest.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts for a password and then shows RestoreKeyBackupDialog", async () => {
|
|
||||||
const result = renderComponent();
|
|
||||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
|
||||||
expect(result.container).toMatchSnapshot();
|
expect(result.container).toMatchSnapshot();
|
||||||
|
|
||||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
// Now we can get the backup and we retry
|
||||||
result.getByRole("button", { name: "Next" }).click();
|
mockClient.getKeyBackupVersion.mockRestore();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||||
expect(modalSpy).toHaveBeenCalledWith(
|
await screen.findByText("Your keys are now being backed up from this device.");
|
||||||
RestoreKeyBackupDialog,
|
|
||||||
{
|
|
||||||
showSummary: false,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
restoreDialogFinishedDefer.resolve([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => {
|
|
||||||
const result = renderComponent();
|
|
||||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
|
||||||
expect(result.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
|
||||||
result.getByRole("button", { name: "Next" }).click();
|
|
||||||
|
|
||||||
expect(modalSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// While we restore the key backup, its signature becomes accepted
|
|
||||||
mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo);
|
|
||||||
|
|
||||||
restoreDialogFinishedDefer.resolve([]);
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
// XXX no idea why this is a sensible thing to do. I just work here.
|
|
||||||
expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled();
|
|
||||||
expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled();
|
|
||||||
|
|
||||||
await result.findByText("Your keys are now being backed up from this device.");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => {
|
|
||||||
filterConsole("Error fetching backup data from server");
|
|
||||||
|
|
||||||
it("handles the error sensibly", async () => {
|
|
||||||
const result = renderComponent();
|
|
||||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
|
||||||
expect(result.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
|
||||||
result.getByRole("button", { name: "Next" }).click();
|
|
||||||
|
|
||||||
expect(modalSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
|
||||||
throw new Error("bleh bleh");
|
|
||||||
});
|
|
||||||
restoreDialogFinishedDefer.resolve([]);
|
|
||||||
await result.findByText("Unable to query secret storage status");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when backup is present but not trusted", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => {
|
|
||||||
const result = renderComponent();
|
|
||||||
await result.findByText("Restore your key backup to upgrade your encryption");
|
|
||||||
expect(result.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
// before we click "Restore", set up a spy on createDialog
|
|
||||||
const restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
|
||||||
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
|
||||||
finished: restoreDialogFinishedDefer.promise,
|
|
||||||
close: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
result.getByRole("button", { name: "Restore" }).click();
|
|
||||||
|
|
||||||
expect(modalSpy).toHaveBeenCalledWith(
|
|
||||||
RestoreKeyBackupDialog,
|
|
||||||
{
|
|
||||||
showSummary: false,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// simulate RestoreKeyBackupDialog completing, to run that code path
|
|
||||||
restoreDialogFinishedDefer.resolve([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
|
exports[`CreateSecretStorageDialog handles the happy path 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
data-focus-guard="true"
|
data-focus-guard="true"
|
||||||
@ -128,47 +128,7 @@ exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no ke
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
exports[`CreateSecretStorageDialog handles the happy path 2`] = `
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-labelledby="mx_BaseDialog_title"
|
|
||||||
class="mx_CreateSecretStorageDialog"
|
|
||||||
data-focus-lock-disabled="false"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dialog_header"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Spinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading…"
|
|
||||||
class="mx_Spinner_icon"
|
|
||||||
data-testid="spinner"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: 32px; height: 32px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
data-focus-guard="true"
|
data-focus-guard="true"
|
||||||
@ -185,105 +145,47 @@ exports[`CreateSecretStorageDialog when backup is present but not trusted shows
|
|||||||
class="mx_Dialog_header"
|
class="mx_Dialog_header"
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
class="mx_Heading_h3 mx_Dialog_title"
|
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_titleWithIcon mx_CreateSecretStorageDialog_secureBackupTitle"
|
||||||
id="mx_BaseDialog_title"
|
id="mx_BaseDialog_title"
|
||||||
>
|
>
|
||||||
Upgrade your encryption
|
Save your Security Key
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<form>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div
|
||||||
<div>
|
class="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer"
|
||||||
<div>
|
>
|
||||||
Restore your key backup to upgrade your encryption
|
<div
|
||||||
</div>
|
class="mx_CreateSecretStorageDialog_recoveryKeyContainer"
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
|
class="mx_CreateSecretStorageDialog_recoveryKey"
|
||||||
|
>
|
||||||
|
<code />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_Dialog_buttons"
|
class="mx_CreateSecretStorageDialog_recoveryKeyButtons"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="mx_Dialog_buttons_row"
|
class="mx_AccessibleButton mx_Dialog_primary mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<button
|
Download
|
||||||
class="danger"
|
</div>
|
||||||
type="button"
|
<span>
|
||||||
>
|
or
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mx_Dialog_primary"
|
|
||||||
data-testid="dialog-primary-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Restore
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
data-focus-guard="true"
|
class="mx_AccessibleButton mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-labelledby="mx_BaseDialog_title"
|
|
||||||
class="mx_CreateSecretStorageDialog"
|
|
||||||
data-focus-lock-disabled="false"
|
|
||||||
role="dialog"
|
|
||||||
>
|
>
|
||||||
<div
|
Copy
|
||||||
class="mx_Dialog_header"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="mx_Heading_h3 mx_Dialog_title"
|
|
||||||
id="mx_BaseDialog_title"
|
|
||||||
>
|
|
||||||
Upgrade your encryption
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form>
|
|
||||||
<p>
|
|
||||||
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>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
Enter your account password to confirm the upgrade:
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="mx_CreateSecretStorageDialog_password"
|
|
||||||
label="Password"
|
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_CreateSecretStorageDialog_password"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -294,196 +196,18 @@ exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls boot
|
|||||||
<span
|
<span
|
||||||
class="mx_Dialog_buttons_row"
|
class="mx_Dialog_buttons_row"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
class="danger"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="mx_Dialog_primary"
|
class="mx_Dialog_primary"
|
||||||
data-testid="dialog-primary-button"
|
data-testid="dialog-primary-button"
|
||||||
disabled=""
|
disabled=""
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Next
|
Continue
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-labelledby="mx_BaseDialog_title"
|
|
||||||
class="mx_CreateSecretStorageDialog"
|
|
||||||
data-focus-lock-disabled="false"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dialog_header"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="mx_Heading_h3 mx_Dialog_title"
|
|
||||||
id="mx_BaseDialog_title"
|
|
||||||
>
|
|
||||||
Upgrade your encryption
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form>
|
|
||||||
<p>
|
|
||||||
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>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
Enter your account password to confirm the upgrade:
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="mx_CreateSecretStorageDialog_password"
|
|
||||||
label="Password"
|
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_CreateSecretStorageDialog_password"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_Dialog_buttons"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_Dialog_buttons_row"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="danger"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mx_Dialog_primary"
|
|
||||||
data-testid="dialog-primary-button"
|
|
||||||
disabled=""
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-focus-guard="true"
|
|
||||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-labelledby="mx_BaseDialog_title"
|
|
||||||
class="mx_CreateSecretStorageDialog"
|
|
||||||
data-focus-lock-disabled="false"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Dialog_header"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="mx_Heading_h3 mx_Dialog_title"
|
|
||||||
id="mx_BaseDialog_title"
|
|
||||||
>
|
|
||||||
Upgrade your encryption
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form>
|
|
||||||
<p>
|
|
||||||
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>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
Enter your account password to confirm the upgrade:
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Field mx_Field_input"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="mx_CreateSecretStorageDialog_password"
|
|
||||||
label="Password"
|
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="mx_CreateSecretStorageDialog_password"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_Dialog_buttons"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_Dialog_buttons_row"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="danger"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mx_Dialog_primary"
|
|
||||||
data-testid="dialog-primary-button"
|
|
||||||
disabled=""
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-focus-guard="true"
|
data-focus-guard="true"
|
||||||
|
Loading…
Reference in New Issue
Block a user