Skip to content

Commit

Permalink
Use matrixClient.secretStorage.getDefaultKeyId instead of `matrixCl…
Browse files Browse the repository at this point in the history
…ient.getCrypto().checkKeyBackupAndEnable` to know if we need to set up a recovery key
  • Loading branch information
florianduros committed Jan 9, 2025
1 parent 7af44cc commit 8bd5d6a
Show file tree
Hide file tree
Showing 8 changed files with 34 additions and 30 deletions.
10 changes: 5 additions & 5 deletions src/components/views/settings/encryption/ChangeRecoveryKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface ChangeRecoveryKeyProps {
* If true, the component will display the flow to change the recovery key.
* If false,the component will display the flow to set up a new recovery key.
*/
userHasKeyBackup: boolean;
userHasRecoveryKey: boolean;
/**
* Called when the recovery key is successfully changed.
*/
Expand All @@ -56,15 +56,15 @@ interface ChangeRecoveryKeyProps {
* A component to set up or change the recovery key.
*/
export function ChangeRecoveryKey({
userHasKeyBackup,
userHasRecoveryKey,
onFinish,
onCancelClick,
}: ChangeRecoveryKeyProps): JSX.Element | null {
const matrixClient = useMatrixClientContext();

// If the user is setting up recovery for the first time, we first show them a panel explaining what
// "recovery" is about. Otherwise, we jump straight to showing the user the new key.
const [state, setState] = useState<State>(userHasKeyBackup ? "save_key_change_flow" : "inform_user");
const [state, setState] = useState<State>(userHasRecoveryKey ? "save_key_change_flow" : "inform_user");

// We create a new recovery key, the recovery key will be displayed to the user
const recoveryKey = useAsyncMemo(() => matrixClient.getCrypto()!.createRecoveryKeyFromPassphrase(), []);
Expand Down Expand Up @@ -110,7 +110,7 @@ export function ChangeRecoveryKey({
// when we will try to access the secret storage during the bootstrap
await withSecretStorageKeyCache(() =>
crypto.bootstrapSecretStorage({
setupNewKeyBackup: !userHasKeyBackup,
setupNewKeyBackup: !userHasRecoveryKey,
setupNewSecretStorage: true,
createSecretStorageKey: async () => recoveryKey,
}),
Expand All @@ -126,7 +126,7 @@ export function ChangeRecoveryKey({

const pages = [
_t("settings|encryption|title"),
userHasKeyBackup
userHasRecoveryKey
? _t("settings|encryption|recovery|change_recovery_key")
: _t("settings|encryption|recovery|set_up_recovery"),
];
Expand Down
19 changes: 11 additions & 8 deletions src/components/views/settings/encryption/RecoveryPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ import { SettingsSubheader } from "../SettingsSubheader";

/**
* The possible states of the recovery panel.
* - `loading`: We are checking the backup, the recovery and the secrets.
* - `missing_backup`: The user has no backup.
* - `loading`: We are checking the recovery key and the secrets.
* - `missing_recovery_key`: The user has no recovery key.
* - `secrets_not_cached`: The user has a backup but the secrets are not cached.
* This shouldn't happen but we have seen cases where the secrets gossiping failed or shared partial secrets when verified with another device.
* - `good`: The user has a backup and the secrets are cached.
*/
type State = "loading" | "missing_backup" | "secrets_not_cached" | "good";
type State = "loading" | "missing_recovery_key" | "secrets_not_cached" | "good";

interface RecoveryPanelProps {
/**
Expand All @@ -38,16 +38,16 @@ interface RecoveryPanelProps {
*/
export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps): JSX.Element {
const [state, setState] = useState<State>("loading");
const isMissingBackup = state === "missing_backup";
const isMissingRecoveryKey = state === "missing_recovery_key";

const matrixClient = useMatrixClientContext();

const checkEncryption = useCallback(async () => {
const crypto = matrixClient.getCrypto()!;

// Check if the user has a backup
const hasBackup = Boolean(await crypto.checkKeyBackupAndEnable());
if (!hasBackup) return setState("missing_backup");
const hasRecoveryKey = Boolean(await matrixClient.secretStorage.getDefaultKeyId());
if (!hasRecoveryKey) return setState("missing_recovery_key");

// Check if the secrets are cached
const cachedSecrets = (await crypto.getCrossSigningStatus()).privateKeysCachedLocally;
Expand All @@ -66,7 +66,7 @@ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps):
case "loading":
content = <InlineSpinner aria-label={_t("common|loading")} />;
break;
case "missing_backup":
case "missing_recovery_key":
content = (
<Button size="sm" kind="primary" Icon={KeyIcon} onClick={() => onChangeRecoveryKeyClick(true)}>
{_t("settings|encryption|recovery|set_up_recovery")}
Expand Down Expand Up @@ -97,7 +97,10 @@ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps):
<SettingsSection
legacy={false}
heading={
<SettingsHeader hasRecommendedTag={isMissingBackup} label={_t("settings|encryption|recovery|title")} />
<SettingsHeader
hasRecommendedTag={isMissingRecoveryKey}
label={_t("settings|encryption|recovery|title")}
/>
}
subHeading={<Subheader state={state} />}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function EncryptionUserSettingsTab(): JSX.Element {
case "set_recovery_key":
content = (
<ChangeRecoveryKey
userHasKeyBackup={state === "change_recovery_key"}
userHasRecoveryKey={state === "change_recovery_key"}
onCancelClick={() => setState("main")}
onFinish={() => setState("main")}
/>
Expand Down
1 change: 1 addition & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function createTestClient(): MatrixClient {
isStored: jest.fn().mockReturnValue(false),
checkKey: jest.fn().mockResolvedValue(false),
hasKey: jest.fn().mockReturnValue(false),
getDefaultKeyId: jest.fn().mockResolvedValue(null),
},

store: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ describe("<ChangeRecoveryKey />", () => {
matrixClient = createTestClient();
});

function renderComponent(userHasKeyBackup = true, onFinish = jest.fn(), onCancelClick = jest.fn()) {
function renderComponent(userHasRecoveryKey = true, onFinish = jest.fn(), onCancelClick = jest.fn()) {
return render(
<ChangeRecoveryKey userHasKeyBackup={userHasKeyBackup} onFinish={onFinish} onCancelClick={onCancelClick} />,
<ChangeRecoveryKey
userHasRecoveryKey={userHasRecoveryKey}
onFinish={onFinish}
onCancelClick={onCancelClick}
/>,
withClientContextRenderOptions(matrixClient),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { KeyBackupCheck } from "matrix-js-sdk/src/crypto-api";

import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
import { RecoveryPanel } from "../../../../../../src/components/views/settings/encryption/RecoveryPanel";
Expand All @@ -36,17 +35,15 @@ describe("<RecoveryPanel />", () => {
);
}

it("should be in loading state when checking backup and the cached keys", () => {
jest.spyOn(matrixClient.getCrypto()!, "checkKeyBackupAndEnable").mockImplementation(
() => new Promise(() => {}),
);
it("should be in loading state when checking the recovery key and the cached keys", () => {
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockImplementation(() => new Promise(() => {}));

const { asFragment } = renderRecoverPanel();
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});

it("should ask to set up a recovery key when there is no key backup", async () => {
it("should ask to set up a recovery key when there is no recovery key", async () => {
const user = userEvent.setup();

const onChangeRecoveryKeyClick = jest.fn();
Expand All @@ -60,7 +57,7 @@ describe("<RecoveryPanel />", () => {
});

it("should ask to enter the recovery key when secrets are not cached", async () => {
jest.spyOn(matrixClient.getCrypto()!, "checkKeyBackupAndEnable").mockResolvedValue({} as KeyBackupCheck);
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
const user = userEvent.setup();
const { asFragment } = renderRecoverPanel();

Expand All @@ -72,7 +69,7 @@ describe("<RecoveryPanel />", () => {
});

it("should allow to change the recovery key when everything is good", async () => {
jest.spyOn(matrixClient.getCrypto()!, "checkKeyBackupAndEnable").mockResolvedValue({} as KeyBackupCheck);
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
privateKeysInSecretStorage: true,
publicKeysOnDevice: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ exports[`<RecoveryPanel /> should ask to enter the recovery key when secrets are
</DocumentFragment>
`;

exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no key backup 1`] = `
exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no recovery key 1`] = `
<DocumentFragment>
<div
class="mx_SettingsSection mx_SettingsSection_newUi"
Expand Down Expand Up @@ -143,7 +143,7 @@ exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no
</DocumentFragment>
`;

exports[`<RecoveryPanel /> should be in loading state when checking backup and the cached keys 1`] = `
exports[`<RecoveryPanel /> should be in loading state when checking the recovery key and the cached keys 1`] = `
<DocumentFragment>
<div
class="mx_SettingsSection mx_SettingsSection_newUi"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { render, screen } from "jest-matrix-react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { waitFor } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import { KeyBackupCheck } from "matrix-js-sdk/src/crypto-api";

import { EncryptionUserSettingsTab } from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab";
import { createTestClient, withClientContextRenderOptions } from "../../../../../../test-utils";
Expand All @@ -22,8 +21,8 @@ describe("<EncryptionUserSettingsTab />", () => {
beforeEach(() => {
matrixClient = createTestClient();
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockResolvedValue(true);
// Key backup is enabled
jest.spyOn(matrixClient.getCrypto()!, "checkKeyBackupAndEnable").mockResolvedValue({} as KeyBackupCheck);
// Recovery key is available
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
// Secrets are cached
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
privateKeysInSecretStorage: true,
Expand Down Expand Up @@ -83,7 +82,7 @@ describe("<EncryptionUserSettingsTab />", () => {
});

it("should display the set up recovery key when the user clicks on the set up recovery key button", async () => {
jest.spyOn(matrixClient.getCrypto()!, "checkKeyBackupAndEnable").mockResolvedValue(null);
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue(null);
const user = userEvent.setup();

const { asFragment } = renderComponent();
Expand Down

0 comments on commit 8bd5d6a

Please sign in to comment.