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:
Florian Duros 2024-09-19 17:39:20 +02:00 committed by GitHub
parent 490746e56a
commit fe657027bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 371 additions and 13 deletions

View file

@ -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",

View file

@ -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";

View file

@ -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,

View file

@ -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) {

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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>
`;