mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 12:28:50 +03:00
Update to use non deprecated methods to decode recovery key (#54)
* Replace `MatrixClient.keyBackupKeyFromRecoveryKey` by `decodeRecoveryKey` * Replace `MatrixClient.isValidRecoveryKey` by local check with `decodeRecoveryKey` * Replace old `decodeRecoveryKey` import * Remove `matrix-js-sdk/src/crypto/recoverykey` import of eslint exception * Add tests for `RestoreKeyBackupDialog`
This commit is contained in:
parent
490746e56a
commit
fe657027bd
7 changed files with 371 additions and 13 deletions
|
@ -122,7 +122,6 @@ module.exports = {
|
||||||
"!matrix-js-sdk/src/crypto/aes",
|
"!matrix-js-sdk/src/crypto/aes",
|
||||||
"!matrix-js-sdk/src/crypto/keybackup",
|
"!matrix-js-sdk/src/crypto/keybackup",
|
||||||
"!matrix-js-sdk/src/crypto/deviceinfo",
|
"!matrix-js-sdk/src/crypto/deviceinfo",
|
||||||
"!matrix-js-sdk/src/crypto/recoverykey",
|
|
||||||
"!matrix-js-sdk/src/crypto/dehydration",
|
"!matrix-js-sdk/src/crypto/dehydration",
|
||||||
"!matrix-js-sdk/src/oidc",
|
"!matrix-js-sdk/src/oidc",
|
||||||
"!matrix-js-sdk/src/oidc/discovery",
|
"!matrix-js-sdk/src/oidc/discovery",
|
||||||
|
|
|
@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
|
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||||
import { deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api";
|
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
|
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { debounce } from "lodash";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { ChangeEvent, FormEvent } from "react";
|
import React, { ChangeEvent, FormEvent } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { SecretStorage } from "matrix-js-sdk/src/matrix";
|
import { SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
|
@ -100,7 +101,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
|
const decodedKey = decodeRecoveryKey(this.state.recoveryKey);
|
||||||
const correct = await cli.checkSecretStorageKey(decodedKey, this.props.keyInfo);
|
const correct = await cli.checkSecretStorageKey(decodedKey, this.props.keyInfo);
|
||||||
this.setState({
|
this.setState({
|
||||||
recoveryKeyValid: true,
|
recoveryKeyValid: true,
|
||||||
|
|
|
@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent } from "react";
|
||||||
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { decodeRecoveryKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
@ -118,10 +118,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
accessSecretStorage(async (): Promise<void> => {}, /* forceReset = */ true);
|
accessSecretStorage(async (): Promise<void> => {}, /* forceReset = */ true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the recovery key is valid
|
||||||
|
* @param recoveryKey
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private isValidRecoveryKey(recoveryKey: string): boolean {
|
||||||
|
try {
|
||||||
|
decodeRecoveryKey(recoveryKey);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onRecoveryKeyChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
private onRecoveryKeyChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
recoveryKey: e.target.value,
|
recoveryKey: e.target.value,
|
||||||
recoveryKeyValid: MatrixClientPeg.safeGet().isValidRecoveryKey(e.target.value),
|
recoveryKeyValid: this.isValidRecoveryKey(e.target.value),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -184,7 +198,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
{ progressCallback: this.progressCallback },
|
{ progressCallback: this.progressCallback },
|
||||||
);
|
);
|
||||||
if (this.props.keyCallback) {
|
if (this.props.keyCallback) {
|
||||||
const key = MatrixClientPeg.safeGet().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
|
const key = decodeRecoveryKey(this.state.recoveryKey);
|
||||||
this.props.keyCallback(key);
|
this.props.keyCallback(key);
|
||||||
}
|
}
|
||||||
if (!this.props.showSummary) {
|
if (!this.props.showSummary) {
|
||||||
|
|
|
@ -58,15 +58,12 @@ describe("AccessSecretStorageDialog", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = getMockClientWithEventEmitter({
|
||||||
keyBackupKeyFromRecoveryKey: jest.fn(),
|
|
||||||
checkSecretStorageKey: jest.fn(),
|
checkSecretStorageKey: jest.fn(),
|
||||||
isValidRecoveryKey: jest.fn(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Closes the dialog when the form is submitted with a valid key", async () => {
|
it("Closes the dialog when the form is submitted with a valid key", async () => {
|
||||||
mockClient.checkSecretStorageKey.mockResolvedValue(true);
|
mockClient.checkSecretStorageKey.mockResolvedValue(true);
|
||||||
mockClient.isValidRecoveryKey.mockReturnValue(true);
|
|
||||||
|
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||||
|
@ -88,8 +85,8 @@ describe("AccessSecretStorageDialog", () => {
|
||||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||||
renderComponent({ onFinished, checkPrivateKey });
|
renderComponent({ onFinished, checkPrivateKey });
|
||||||
|
|
||||||
mockClient.keyBackupKeyFromRecoveryKey.mockImplementation(() => {
|
mockClient.checkSecretStorageKey.mockImplementation(() => {
|
||||||
throw new Error("that's no key");
|
throw new Error("invalid key");
|
||||||
});
|
});
|
||||||
|
|
||||||
await enterSecurityKey();
|
await enterSecurityKey();
|
||||||
|
@ -115,7 +112,6 @@ describe("AccessSecretStorageDialog", () => {
|
||||||
};
|
};
|
||||||
const checkPrivateKey = jest.fn().mockResolvedValue(false);
|
const checkPrivateKey = jest.fn().mockResolvedValue(false);
|
||||||
renderComponent({ checkPrivateKey, keyInfo });
|
renderComponent({ checkPrivateKey, keyInfo });
|
||||||
mockClient.isValidRecoveryKey.mockReturnValue(false);
|
|
||||||
|
|
||||||
await enterSecurityKey("Security Phrase");
|
await enterSecurityKey("Security Phrase");
|
||||||
expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey);
|
expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { screen, render, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
// Needed to be able to mock decodeRecoveryKey
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import * as recoveryKeyModule from "matrix-js-sdk/src/crypto-api/recovery-key";
|
||||||
|
|
||||||
|
import RestoreKeyBackupDialog from "../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx";
|
||||||
|
import { stubClient } from "../../../../test-utils";
|
||||||
|
|
||||||
|
describe("<RestoreKeyBackupDialog />", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render", async () => {
|
||||||
|
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||||
|
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display an error when recovery key is invalid", async () => {
|
||||||
|
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockImplementation(() => {
|
||||||
|
throw new Error("Invalid recovery key");
|
||||||
|
});
|
||||||
|
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||||
|
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "invalid key");
|
||||||
|
await waitFor(() => expect(screen.getByText("👎 Not a valid Security Key")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not raise an error when recovery is valid", async () => {
|
||||||
|
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||||
|
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "valid key");
|
||||||
|
await waitFor(() => expect(screen.getByText("👍 This looks like a valid Security Key!")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,298 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RestoreKeyBackupDialog /> should display an error when recovery key is invalid 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<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_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Enter Security Key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_content"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<b>
|
||||||
|
Warning
|
||||||
|
</b>
|
||||||
|
: you should only set up key backup from a trusted computer.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||||
|
value="invalid key"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||||
|
>
|
||||||
|
👎 Not a valid Security Key
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
If you've forgotten your Security Key you can
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
set up new recovery options
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="Close dialog"
|
||||||
|
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RestoreKeyBackupDialog /> should not raise an error when recovery is valid 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<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_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Enter Security Key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_content"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<b>
|
||||||
|
Warning
|
||||||
|
</b>
|
||||||
|
: you should only set up key backup from a trusted computer.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||||
|
value="valid key"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||||
|
>
|
||||||
|
👍 This looks like a valid Security Key!
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
If you've forgotten your Security Key you can
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
set up new recovery options
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="Close dialog"
|
||||||
|
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RestoreKeyBackupDialog /> should render 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<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_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Enter Security Key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_content"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<b>
|
||||||
|
Warning
|
||||||
|
</b>
|
||||||
|
: you should only set up key backup from a trusted computer.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
If you've forgotten your Security Key you can
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
set up new recovery options
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="Close dialog"
|
||||||
|
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
Loading…
Reference in a new issue