diff --git a/.eslintrc.js b/.eslintrc.js index 9cb3e29fb7..a3c7eb4f8d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,7 +122,6 @@ module.exports = { "!matrix-js-sdk/src/crypto/aes", "!matrix-js-sdk/src/crypto/keybackup", "!matrix-js-sdk/src/crypto/deviceinfo", - "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", "!matrix-js-sdk/src/oidc/discovery", diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index f03f83d573..fb73e25389 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -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 { deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api"; -import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; +import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog"; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 3759e11063..0c4e875607 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -10,6 +10,7 @@ import { debounce } from "lodash"; import classNames from "classnames"; import React, { ChangeEvent, FormEvent } from "react"; 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 { MatrixClientPeg } from "../../../../MatrixClientPeg"; @@ -100,7 +101,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent => {}, /* 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): void => { this.setState({ 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 { beforeEach(() => { mockClient = getMockClientWithEventEmitter({ - keyBackupKeyFromRecoveryKey: jest.fn(), checkSecretStorageKey: jest.fn(), - isValidRecoveryKey: jest.fn(), }); }); it("Closes the dialog when the form is submitted with a valid key", async () => { mockClient.checkSecretStorageKey.mockResolvedValue(true); - mockClient.isValidRecoveryKey.mockReturnValue(true); const onFinished = jest.fn(); const checkPrivateKey = jest.fn().mockResolvedValue(true); @@ -88,8 +85,8 @@ describe("AccessSecretStorageDialog", () => { const checkPrivateKey = jest.fn().mockResolvedValue(true); renderComponent({ onFinished, checkPrivateKey }); - mockClient.keyBackupKeyFromRecoveryKey.mockImplementation(() => { - throw new Error("that's no key"); + mockClient.checkSecretStorageKey.mockImplementation(() => { + throw new Error("invalid key"); }); await enterSecurityKey(); @@ -115,7 +112,6 @@ describe("AccessSecretStorageDialog", () => { }; const checkPrivateKey = jest.fn().mockResolvedValue(false); renderComponent({ checkPrivateKey, keyInfo }); - mockClient.isValidRecoveryKey.mockReturnValue(false); await enterSecurityKey("Security Phrase"); expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey); diff --git a/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx b/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx new file mode 100644 index 0000000000..3e52b473b6 --- /dev/null +++ b/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx @@ -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("", () => { + beforeEach(() => { + stubClient(); + jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32)); + }); + + it("should render", async () => { + const { asFragment } = render(); + 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(); + 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(); + 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(); + }); +}); diff --git a/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap b/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap new file mode 100644 index 0000000000..de0bddbe33 --- /dev/null +++ b/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap @@ -0,0 +1,298 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should display an error when recovery key is invalid 1`] = ` + +
+